├── .editorconfig ├── .scrutinizer.yml ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Bootstrap.php ├── Collection.php ├── Criteria ├── Common │ ├── Field.php │ └── FieldInterface.php ├── Criterion.php ├── CriterionInterface.php ├── GroupBy.php ├── Having.php ├── HavingGroup.php ├── Join.php ├── Limit.php ├── Offset.php ├── OrderBy.php ├── Relation.php ├── Selection.php ├── Where.php ├── Where │ └── Operator.php └── WhereGroup.php ├── Fun ├── FieldFunction.php └── RawFunction.php ├── HighOrderMessaging └── HigherOrderCollectionProxy.php ├── Hydrogen.php ├── Processor ├── BuilderInterface.php ├── DatabaseProcessor.php ├── DatabaseProcessor │ ├── Builder.php │ ├── Common │ │ └── Expression.php │ ├── GroupBuilder.php │ ├── GroupByBuilder.php │ ├── HavingBuilder.php │ ├── HavingGroupBuilder.php │ ├── JoinBuilder.php │ ├── LimitBuilder.php │ ├── OffsetBuilder.php │ ├── OrderByBuilder.php │ ├── RelationBuilder.php │ ├── SelectBuilder.php │ └── WhereBuilder.php ├── Processor.php ├── ProcessorInterface.php └── Queue.php ├── Query.php ├── Query ├── AliasProvider.php ├── ExecutionsProvider.php ├── GroupByProvider.php ├── LimitAndOffsetProvider.php ├── ModeProvider.php ├── OrderProvider.php ├── RelationProvider.php ├── RepositoryProvider.php ├── SelectProvider.php ├── WhereProvider.php └── WhereProvider │ ├── WhereBetweenProvider.php │ ├── WhereInProvider.php │ ├── WhereLikeProvider.php │ └── WhereNullProvider.php └── helpers.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | php: 3 | code_rating: true 4 | duplication: true 5 | fix_php_opening_tag: true 6 | remove_php_closing_tag: true 7 | one_class_per_file: true 8 | side_effects_or_types: true 9 | no_mixed_inline_html: true 10 | require_braces_around_control_structures: true 11 | php5_style_constructor: true 12 | no_global_keyword: true 13 | avoid_usage_of_logical_operators: true 14 | psr2_class_declaration: true 15 | no_underscore_prefix_in_properties: true 16 | no_underscore_prefix_in_methods: true 17 | blank_line_after_namespace_declaration: true 18 | single_namespace_per_use: true 19 | psr2_switch_declaration: true 20 | psr2_control_structure_declaration: true 21 | avoid_superglobals: true 22 | security_vulnerabilities: true 23 | no_exit: true 24 | coding_style: 25 | php: 26 | braces: 27 | classes_functions: 28 | class: new-line 29 | function: new-line 30 | closure: end-of-line 31 | if: 32 | opening: end-of-line 33 | for: 34 | opening: end-of-line 35 | while: 36 | opening: end-of-line 37 | do_while: 38 | opening: end-of-line 39 | switch: 40 | opening: end-of-line 41 | try: 42 | opening: end-of-line 43 | upper_lower_casing: 44 | keywords: 45 | general: lower 46 | constants: 47 | true_false_null: lower 48 | filter: 49 | paths: ["src/*"] 50 | 51 | build: 52 | environment: 53 | php: 54 | version: 7.1 55 | tests: 56 | override: 57 | - 58 | command: 'vendor/bin/phpunit --coverage-clover=clover.xml' 59 | coverage: 60 | file: 'clover.xml' 61 | format: 'clover' 62 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2017-2018 Rambler Digital Solutions 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Hydrogen 3 |

4 | 5 |

6 | Travis CI 7 | Code coverage 8 | Scrutinizer CI 9 | Latest Stable Version 10 | Latest Unstable Version 11 | License MIT 12 |

13 | 14 | - [Introduction](#introduction) 15 | - [Installation](#installation) 16 | - [Server Requirements](#server-requirements) 17 | - [Installing Hydrogen](#installing-hydrogen) 18 | - [Usage](#usage) 19 | - [Retrieving Results](#retrieving-results) 20 | - [Retrieving All Entities](#retrieving-all-entities) 21 | - [Retrieving A Single Entity](#retrieving-a-single-entity) 22 | - [Retrieving A List Of Field Values](#retrieving-a-list-of-field-values) 23 | - [Aggregates And Scalar Results](#aggregates-and-scalar-results) 24 | - [Selects](#selects) 25 | - [Additional fields](#additional-fields) 26 | - [Where Clauses](#where-clauses) 27 | - [Simple Where Clauses](#simple-where-clauses) 28 | - [Or Statements](#or-statements) 29 | - [Additional Where Clauses](#additional-where-clauses) 30 | - [Parameter Grouping](#parameter-grouping) 31 | - [Ordering](#ordering) 32 | - [Grouping](#grouping) 33 | - [Limit And Offset](#limit-and-offset) 34 | - [Embeddables](#embeddables) 35 | - [Relations](#relations) 36 | - [Joins](#joins) 37 | - [Joins Subqueries](#joins-subqueries) 38 | - [Nested Relationships](#nested-relationships) 39 | - [Query Scopes](#query-scopes) 40 | - [Collections](#collections) 41 | - [Higher Order Messaging](#higher-order-messaging) 42 | - [Destructuring](#destructuring) 43 | 44 | ## Introduction 45 | 46 | Hydrogen provides a beautiful, convenient and simple implementation for 47 | working with Doctrine queries. It does not affect the existing code 48 | in any way and can be used even in pre-built production applications. 49 | 50 | ## Installation 51 | 52 | ### Server Requirements 53 | 54 | The Hydrogen library has several system requirements. 55 | You need to make sure that your server meets the following requirements: 56 | 57 | - PHP >= 7.1.3 58 | - PDO PHP Extension 59 | - Mbstring PHP Extension 60 | - JSON PHP Extension 61 | - [doctrine/orm >= 2.5](https://packagist.org/packages/doctrine/orm) 62 | - [illuminate/support >= 5.5](https://packagist.org/packages/illuminate/support) 63 | 64 | ### Installing Hydrogen 65 | 66 | Hydrogen utilizes [Composer](https://getcomposer.org/) to manage its dependencies. 67 | So, before using Hydrogen, make sure you have Composer installed on your machine. 68 | 69 | **Stable** 70 | 71 | ```bash 72 | composer require rds/hydrogen 73 | ``` 74 | 75 | **Dev** 76 | 77 | ```bash 78 | composer require rds/hydrogen dev-master@dev 79 | ``` 80 | 81 | ## Usage 82 | 83 | Hydrogen interacts with the repositories of the Doctrine. 84 | In order to take advantage of additional features - you need to 85 | add the main trait to an existing implementation of the repository. 86 | 87 | ```php 88 | query()` method on the Repository to begin a query. 106 | This method returns a fluent query builder instance for the given repository, 107 | allowing you to chain more constraints onto the query and then finally 108 | get the results using the `->get()` method: 109 | 110 | ```php 111 | query->get(); 123 | } 124 | } 125 | ``` 126 | 127 | The `get()` method returns an `array` containing the results, 128 | where each result is an instance of the object (Entity) associated 129 | with the specified repository: 130 | 131 | ```php 132 | foreach ($users->toArray() as $user) { 133 | \var_dump($user); 134 | } 135 | ``` 136 | 137 | In addition, you can use the method `collect()` to 138 | get a collection that is compatible with ArrayCollection: 139 | 140 | ```php 141 | query->collect(); 154 | } 155 | } 156 | ``` 157 | 158 | ```php 159 | $users->toCollection()->each(function (User $user): void { 160 | \var_dump($user); 161 | }); 162 | ``` 163 | 164 | > **Note:** Direct access to the Hydrogen build, instead of the 165 | existing methods, which is provided by the Doctrine completely 166 | **ignores** all relations (like: `@OneToMany(..., fetch="EAGER")`). 167 | 168 | ### Retrieving A Single Entity 169 | 170 | If you just need to retrieve a single row from the database table, 171 | you may use the first method. This method will return a single Entity object: 172 | 173 | ```php 174 | $user = $repository->query->where('name', 'John')->first(); 175 | 176 | echo $user->getName(); 177 | ``` 178 | 179 | If you don't even need an entire row, you may extract a single 180 | values from a record using additional arguments for `->first()` method. 181 | This method will return the value of the column directly: 182 | 183 | ```php 184 | [$name, $email] = $repository->query->where('name', 'John')->first('name', 'email'); 185 | 186 | echo $name . ' with email ' . $email; 187 | ``` 188 | 189 | ### Retrieving A List Of Field Values 190 | 191 | If you would like to retrieve an array or Collection containing the values of a single Entity's field value, 192 | you may use the additional arguments for `->get()` or `->collect()` methods. 193 | In this example, we'll retrieve a Collection of user ids and names: 194 | 195 | ```php 196 | $users = $repository->query->get('id', 'name'); 197 | 198 | foreach ($users as ['id' => $id, 'name' => $name]) { 199 | echo $id . ': ' . $name; 200 | } 201 | ``` 202 | 203 | ### Aggregates and Scalar Results 204 | 205 | The query builder also provides a variety of aggregate methods such as `count`, `max`, `min`, 206 | `avg`, and `sum`. You may call any of these methods after constructing your query: 207 | 208 | ```php 209 | $count = $users->query->count(); 210 | 211 | $price = $prices->query->max('price'); 212 | ``` 213 | 214 | Of course, you may combine these methods with other clauses: 215 | 216 | ```php 217 | $price = $prices->query 218 | ->where('user', $user) 219 | ->where('finalized', 1) 220 | ->avg('price'); 221 | ``` 222 | 223 | In the event that your database supports any other functions, 224 | then you can use these methods directly using `->scalar()` method: 225 | 226 | The first argument of the `->scalar()` method requires specifying the field that should be 227 | contained in the result. The second optional argument allows you 228 | to convert the type to the desired one. 229 | 230 | ```php 231 | $price = $prices->query 232 | ->select('AVG(price) as price') 233 | ->scalar('price', 'int'); 234 | ``` 235 | 236 | **Allowed Types** 237 | 238 | | Type | Description | 239 | |------------|----------------------------------| 240 | | `int` | Returns an integer value | 241 | | `float` | Returns a float value | 242 | | `string` | Returns a string value | 243 | | `bool` | Returns boolean value | 244 | | `callable` | Returns the Closure instance | 245 | | `object` | Returns an object | 246 | | `array` | Returns an array | 247 | | `iterable` | `array` alias | 248 | 249 | **Query Invocations** 250 | 251 | | Method | Description | 252 | |------------|------------------------------------------| 253 | | `get` | Returns an array of entities | 254 | | `collect` | Returns a Collection of entities | 255 | | `first` | Returns the first result | 256 | | `scalar` | Returns the single scalar value | 257 | | `count` | Returns count of given field | 258 | | `sum` | Returns sum of given field | 259 | | `avg` | Returns average of given field | 260 | | `max` | Returns max value of given field | 261 | | `min` | Returns min value of given field | 262 | 263 | ## Selects 264 | 265 | Using the `select()` method, you can specify a 266 | custom select clause for the query: 267 | 268 | ```php 269 | ['count' => $count] = $users->query 270 | ->select(['COUNT(id)' => 'count']) 271 | ->get(); 272 | 273 | echo $count; 274 | ``` 275 | 276 | Also, this expression can be simplified 277 | and rewritten in this way: 278 | 279 | ```php 280 | $result = $users->query 281 | ->select(['COUNT(id)' => 'count']) 282 | ->scalar('count'); 283 | 284 | echo $result; 285 | ``` 286 | 287 | ### Additional fields 288 | 289 | **Entity** 290 | 291 | You noticed that if we specify a select, then in the response we get the data 292 | of the select, ignoring the Entity. In order to get any entity in the response, 293 | we should use the method `withEntity`: 294 | 295 | ```php 296 | ['messages' => $messages, 'user' => $user] = $users->query 297 | ->select(['COUNT(messages)' => 'messages']) 298 | ->withEntity('user') 299 | ->where('id', 23) 300 | ->first(); 301 | ``` 302 | 303 | **Raw Columns** 304 | 305 | Sometimes some fields may not be contained in Entity, for example, 306 | relation keys. In this case, we have no choice but to choose this 307 | columns directly, bypassing the structure of the Entity: 308 | 309 | ```php 310 | $messages = $query 311 | ->select([$query->column('user_id') => 'user_id']) 312 | ->withEntity('message') 313 | ->get('message', 'user_id'); 314 | 315 | foreach ($messages as ['message' => $message, 'user_id' => $id]) { 316 | echo $message->title . ' of user #' . $id; 317 | } 318 | ``` 319 | 320 | 321 | ## Where Clauses 322 | 323 | ### Simple Where Clauses 324 | 325 | You may use the where method on a query builder instance to add 326 | where clauses to the query. The most basic call to where requires 327 | three arguments. The first argument is the name of the column. 328 | The second argument is an operator, which can be any of the 329 | database's supported operators. Finally, the third argument is 330 | the value to evaluate against the column. 331 | 332 | For example, here is a query that verifies the value of the 333 | "votes" Entity field is equal to 100: 334 | 335 | ```php 336 | $users = $repository->query->where('votes', '=', 100)->get(); 337 | ``` 338 | 339 | For convenience, if you want to verify that a column is equal 340 | to a given value, you may pass the value directly as the 341 | second argument to the where method: 342 | 343 | ```php 344 | $users = $repository->query->where('votes', 100)->get(); 345 | ``` 346 | 347 | Of course, you may use a variety of other operators when 348 | writing a where clause: 349 | 350 | ```php 351 | $users = $repository->query 352 | ->where('votes', '>=', 100) 353 | ->get(); 354 | 355 | $users = $repository->query 356 | ->where('votes', '<>', 100) 357 | ->get(); 358 | 359 | $users = $repository->query 360 | ->where('votes', '<=', 100) 361 | ->get(); 362 | ``` 363 | 364 | ### Or Statements 365 | 366 | You may chain where constraints together as well as add `or` 367 | clauses to the query. The `orWhere` method accepts the same 368 | arguments as the where method: 369 | 370 | ```php 371 | $users = $repository->query 372 | ->where('votes', '>', 100) 373 | ->orWhere('name', 'John') 374 | ->get(); 375 | ``` 376 | 377 | Alternatively, you can use the `->or` magic method: 378 | 379 | ```php 380 | $users = $repository->query 381 | ->where('votes', '>', 100) 382 | ->or->where('name', 'John') 383 | ->get(); 384 | ``` 385 | 386 | ### Additional Where Clauses 387 | 388 | **whereBetween** 389 | 390 | The `whereBetween` method verifies that a Entity fields's value is between two values: 391 | 392 | ```php 393 | $users = $repository->query 394 | ->whereBetween('votes', 1, 100) 395 | ->get(); 396 | 397 | $users = $repository->query 398 | ->where('name', 'John') 399 | ->orWhereBetween('votes', 1, 100) 400 | ->get(); 401 | ``` 402 | 403 | **whereNotBetween** 404 | 405 | The `whereNotBetween` method verifies that a Entity field's value lies outside of two values: 406 | 407 | ```php 408 | $users = $repository->query 409 | ->whereNotBetween('votes', 1, 100) 410 | ->get(); 411 | 412 | $users = $repository->query 413 | ->where('name', 'John') 414 | ->orWhereNotBetween('votes', 1, 100) 415 | ->get(); 416 | ``` 417 | 418 | **whereIn / whereNotIn** 419 | 420 | The `whereIn` method verifies that a given Entity field's value 421 | is contained within the given array: 422 | 423 | ```php 424 | $users = $repository->query 425 | ->whereIn('id', [1, 2, 3]) 426 | ->get(); 427 | 428 | $users = $repository->query 429 | ->where('id', [1, 2, 3]) 430 | // Equally: ->whereIn('id', [1, 2, 3]) 431 | ->orWhere('id', [101, 102, 103]) 432 | // Equally: ->orWhereIn('id', [101, 102, 103]) 433 | ->get(); 434 | ``` 435 | 436 | The `whereNotIn` method verifies that the given Entity field's value 437 | is not contained in the given array: 438 | 439 | ```php 440 | $users = $repository->query 441 | ->whereNotIn('id', [1, 2, 3]) 442 | ->get(); 443 | 444 | $users = $repository->query 445 | ->where('id', '<>', [1, 2, 3]) 446 | // Equally: ->whereNotIn('id', [1, 2, 3]) 447 | ->orWhere('id', '<>', [101, 102, 103]) 448 | // Equally: ->orWhereNotIn('id', [101, 102, 103]) 449 | ->get(); 450 | ``` 451 | 452 | **whereNull / whereNotNull** 453 | 454 | The `whereNull` method verifies that the value of 455 | the given Entity field is `NULL`: 456 | 457 | ```php 458 | $users = $repository->query 459 | ->whereNull('updatedAt') 460 | ->get(); 461 | 462 | $users = $repository->query 463 | ->where('updatedAt', null) 464 | // Equally: ->whereNull('updatedAt') 465 | ->orWhereNull('deletedAt', null) 466 | // Equally: ->orWhereNull('deletedAt') 467 | ->get(); 468 | ``` 469 | 470 | The `whereNotNull` method verifies that 471 | the Entity field's value is not `NULL`: 472 | 473 | ```php 474 | $users = $repository->query 475 | ->whereNotNull('updatedAt') 476 | ->get(); 477 | 478 | $users = $repository->query 479 | ->whereNotNull('updatedAt') 480 | ->or->whereNotNull('deletedAt') 481 | ->get(); 482 | ``` 483 | 484 | **like / notLike** 485 | 486 | The `like` method verifies that the value of 487 | the given Entity field like given value: 488 | 489 | ```php 490 | $messages = $repository->query 491 | ->like('description', '%some%') 492 | ->orLike('description', '%any%') 493 | ->get(); 494 | 495 | $messages = $repository->query 496 | ->where('description', '~', '%some%') 497 | ->orWhere('description', '~', '%any%') 498 | ->get(); 499 | ``` 500 | 501 | The `notLike` method verifies that the value of 502 | the given Entity field is not like given value: 503 | 504 | ```php 505 | $messages = $repository->query 506 | ->notLike('description', '%some%') 507 | ->orNotLike('description', '%any%') 508 | ->get(); 509 | 510 | $messages = $repository->query 511 | ->where('description', '!~', '%some%') 512 | ->orWhere('description', '!~', '%any%') 513 | ->get(); 514 | ``` 515 | 516 | ### Parameter Grouping 517 | 518 | Sometimes you may need to create more advanced where 519 | clauses such as "where exists" clauses or nested parameter 520 | groupings. The Hydrogen query builder can handle these as well. 521 | To get started, let's look at an example of grouping 522 | constraints within parenthesis: 523 | 524 | ```php 525 | $users = $repository->query 526 | ->where('name', 'John') 527 | ->where(function (Query $query): void { 528 | $query->where('votes', '>', 100) 529 | ->orWhere('title', 'Admin'); 530 | }) 531 | ->get(); 532 | ``` 533 | 534 | As you can see, passing a `Closure` into the `where` method 535 | instructs the query builder to begin a constraint group. 536 | The `Closure` will receive a query builder instance which 537 | you can use to set the constraints that should be contained 538 | within the parenthesis group. The example above will 539 | produce the following DQL: 540 | 541 | ```sql 542 | SELECT u FROM App\Entity\User u 543 | WHERE u.name = "John" AND ( 544 | u.votes > 100 OR 545 | u.title = "Admin" 546 | ) 547 | ``` 548 | 549 | In addition to this, instead of the `where` or `orWhere` method, 550 | you can use another options. Methods `or` and `and` will do the same: 551 | 552 | ```php 553 | $users = $repository->query 554 | ->where('name', 'John') 555 | ->and(function (Query $query): void { 556 | $query->where('votes', '>', 100) 557 | ->orWhere('title', 'Admin'); 558 | }) 559 | ->get(); 560 | 561 | // SELECT u FROM App\Entity\User u 562 | // WHERE u.name = "John" AND ( 563 | // u.votes > 100 OR 564 | // u.title = "Admin" 565 | // ) 566 | 567 | $users = $repository->query 568 | ->where('name', 'John') 569 | ->or(function (Query $query): void { 570 | $query->where('votes', '>', 100) 571 | ->where('title', 'Admin'); 572 | }) 573 | ->get(); 574 | 575 | // SELECT u FROM App\Entity\User u 576 | // WHERE u.name = "John" OR ( 577 | // u.votes > 100 AND 578 | // u.title = "Admin" 579 | // ) 580 | ``` 581 | 582 | ## Ordering 583 | 584 | **orderBy** 585 | 586 | The `orderBy` method allows you to sort the result of the query 587 | by a given column. The first argument to the `orderBy` method 588 | should be the column you wish to sort by, while the second argument 589 | controls the direction of the sort and may be either asc or desc: 590 | 591 | ```php 592 | $users = $repository->query 593 | ->orderBy('name', 'desc') 594 | ->get(); 595 | ``` 596 | 597 | Also, you may use shortcuts `asc()` and `desc()` to simplify the code: 598 | 599 | ```php 600 | $users = $repository->query 601 | ->asc('id', 'createdAt') 602 | ->desc('name') 603 | ->get(); 604 | ``` 605 | 606 | **latest / oldest** 607 | 608 | The latest and oldest methods allow you to easily order 609 | results by date. By default, result will be ordered by the 610 | `createdAt` Entity field. Or, you may pass the column name 611 | that you wish to sort by: 612 | 613 | ```php 614 | $users = $repository->query 615 | ->latest() 616 | ->get(); 617 | 618 | $posts = $repository->query 619 | ->oldest('updatedAt') 620 | ->get(); 621 | ``` 622 | 623 | ## Grouping 624 | 625 | **groupBy** 626 | 627 | The `groupBy` method may be used to group the query results: 628 | 629 | ```php 630 | $users = $repository->query 631 | ->groupBy('account') 632 | ->get(); 633 | ``` 634 | 635 | You may pass multiple arguments to the `groupBy` method to group by 636 | multiple columns: 637 | 638 | ```php 639 | $users = $repository->query 640 | ->groupBy('firstName', 'status') 641 | ->get(); 642 | ``` 643 | 644 | **having** 645 | 646 | The `having` method's signature is similar to that 647 | of the `where` method: 648 | 649 | ```php 650 | $users = $repository->query 651 | ->groupBy('account') 652 | ->having('account.id', '>', 100) 653 | ->get(); 654 | ``` 655 | 656 | ## Limit And Offset 657 | 658 | **skip / take** 659 | 660 | To limit the number of results returned from the query, or 661 | to skip a given number of results in the query, you may 662 | use the `skip()` and `take()` methods: 663 | 664 | ```php 665 | $users = $repository->query->skip(10)->take(5)->get(); 666 | ``` 667 | 668 | Alternatively, you may use the `limit` and `offset` methods: 669 | 670 | ```php 671 | $users = $repository->query 672 | ->offset(10) 673 | ->limit(5) 674 | ->get(); 675 | ``` 676 | 677 | **before / after** 678 | 679 | Usually during a heavy load on the DB, the `offset` can shift while 680 | inserting new records into the table. In this case it is worth using 681 | the methods of `before()` and `after()` to ensure that the subsequent 682 | sample will be strictly following the previous one. 683 | 684 | Let's give an example of obtaining 10 articles, 685 | which are located after the id 15: 686 | 687 | ```php 688 | $articles = $repository->query 689 | ->where('category', 'news') 690 | ->after('id', 15) 691 | ->take(10) 692 | ->get(); 693 | ``` 694 | 695 | **range** 696 | 697 | You may use the `range()` method to specify exactly which 698 | record you want to receive as a result: 699 | 700 | ```php 701 | $articles = $repository->range(10, 20)->get(); 702 | ``` 703 | 704 | ## Embeddables 705 | 706 | Embeddables are classes which are not entities themselves, but are 707 | embedded in entities and can also be queried by Hydrogen. 708 | You'll mostly want to use them to reduce duplication or separating concerns. 709 | Value objects such as date range or address are the primary use 710 | case for this feature. 711 | 712 | ```php 713 | query->asc('address.country')->get(); 755 | } 756 | } 757 | ``` 758 | 759 | ## Relations 760 | 761 | The Doctrine ORM provides several types of different relations: `@OneToOne`, 762 | `@OneToMany`, `@ManyToOne` and `@ManyToMany`. And "greed" for loading these 763 | relations is set at the metadata level of the entities. The Doctrine 764 | does not provide the ability to manage relations and load them 765 | during querying, so when you retrieve the data, you can encounter 766 | `N+1` queries without the use of DQL, especially 767 | on `@OneToOne` relations, where there is simply no other loading option. 768 | 769 | The Hydrogen allows you to flexibly manage how to obtain relations at 770 | the query level, as well as their number and additional aggregate functions 771 | applicable to these relationships: 772 | 773 | ```php 774 | query 816 | ->join('cart') 817 | ->get(); 818 | 819 | foreach ($customers as $customer) { 820 | echo $customer->cart->id; 821 | } 822 | ``` 823 | 824 | > **Please note** that when using joins, you can not use `limit`, because it affects 825 | the total amount of data in the response (i.e., including relations), rather than the 826 | number of parent entities. 827 | 828 | ### Joins Subqueries 829 | 830 | We can also work with additional operations on dependent entities. 831 | For example, we want to get a list of users (customers) who have more than 832 | 100 rubles on their balance sheet: 833 | 834 | ```php 835 | $customers = $customerRepository->query 836 | ->join(['cart' => function (Query $query): void { 837 | $query->where('balance', '>', 100) 838 | ->where('currency', 'RUB'); 839 | }]) 840 | ->get(); 841 | ``` 842 | 843 | > **Note**: Operations using `join` affect the underlying query. 844 | 845 | ### Nested Relationships 846 | 847 | So, if we need all the customers that have been ordered, 848 | for example, movie tickets, we need to make a simple request: 849 | 850 | ```php 851 | $customers = $customerRepository->query 852 | ->join(['cart.goods' => function (Query $query): void { 853 | $query->where('category', 'tickets') 854 | ->where('value', '>', 0); 855 | }]) 856 | ->get(); 857 | ``` 858 | 859 | ## Query Scopes 860 | 861 | Sometimes it takes a long time to build a whole query, and some parts of 862 | it already repeat existing ones. In this case, we can use the mechanism 863 | of scopes, which allows you to add a set of methods to the query, 864 | which in turn must return parts of the query we need: 865 | 866 | ```php 867 | query->whereNotNull('bannedAt') 877 | : $this->query->whereNull('bannedAt'); 878 | } 879 | 880 | public function findBanned(): iterable 881 | { 882 | // We supplement the query, call the existing method "banned" 883 | return $this->query->banned->get(); 884 | } 885 | 886 | public function findActive(): iterable 887 | { 888 | // We supplement the query, call the existing method "banned" with additional argument "false" 889 | return $this->query->banned(false)->get(); 890 | } 891 | } 892 | ``` 893 | 894 | ## Collections 895 | 896 | As the base kernel used a [Illuminate Collections](https://laravel.com/docs/5.5/collections) but 897 | some new features have been added: 898 | 899 | - Add HOM proxy autocomplete. 900 | - Added support for global function calls using the [Higher Order Messaging](https://en.wikipedia.org/wiki/Higher_order_message) 901 | and the [Pattern Matching](https://en.wikipedia.org/wiki/Pattern_matching). 902 | 903 | ### Higher Order Messaging 904 | 905 | Pattern "`_`" is used to specify the location of the delegate in 906 | the function arguments in the higher-order messaging while using global functions. 907 | 908 | ```php 909 | use RDS\Hydrogen\Collection; 910 | 911 | $data = [ 912 | ['value' => '23'], 913 | ['value' => '42'], 914 | ['value' => 'Hello!'], 915 | ]; 916 | 917 | 918 | $example1 = Collection::make($data) 919 | ->map->value // ['23', '42', 'Hello!'] 920 | ->toArray(); 921 | 922 | // 923 | // $example1 = \array_map(function (array $item): string { 924 | // return $item['value']; 925 | // }, $data); 926 | // 927 | 928 | $example2 = Collection::make($data) 929 | ->map->value // ['23', '42', 'Hello!'] 930 | ->map->intval(_) // [23, 42, 0] 931 | ->filter() // [23, 42] 932 | ->toArray(); 933 | 934 | // 935 | // 936 | // $example2 = \array_map(function (array $item): string { 937 | // return $item['value']; 938 | // }, $data); 939 | // 940 | // $example2 = \array_map(function (string $value): int { 941 | // return \intval($value); 942 | // ^^^^^ - pattern "_" will replaced to each delegated item value. 943 | // }, $example1); 944 | // 945 | // $example2 = \array_filter($example2, function(int $value): bool { 946 | // return (bool)$value; 947 | // }); 948 | // 949 | // 950 | 951 | $example3 = Collection::make($data) 952 | ->map->value // ['23', '42', 'Hello!'] 953 | ->map->mbSubstr(_, 1) // Using "mb_substr(_, 1)" -> ['3', '2', 'ello!'] 954 | ->toArray(); 955 | ``` 956 | 957 | ### Destructuring 958 | 959 | ```php 960 | use RDS\Hydrogen\Collection; 961 | 962 | $collection = Collection::make([ 963 | ['a' => 'A1', 'b' => 'B1' 'value' => '23'], 964 | ['a' => 'A2', 'b' => 'B2' 'value' => '42'], 965 | ['a' => 'A3', 'b' => 'B3' 'value' => 'Hello!'], 966 | ]); 967 | 968 | // Displays all data 969 | foreach($collection as $item) { 970 | \var_dump($item); // [a => 'A*', b => 'B*', value => '***'] 971 | } 972 | 973 | // Displays only "a" field 974 | foreach ($collection as ['a' => $a]) { 975 | \var_dump($a); // 'A' 976 | } 977 | ``` 978 | 979 | -------------------- 980 | 981 | Beethoven approves. 982 | 983 | ![https://habrastorage.org/webt/lf/hw/dn/lfhwdnvjxlt9vrsbrd_ajpitubc.png](https://habrastorage.org/webt/lf/hw/dn/lfhwdnvjxlt9vrsbrd_ajpitubc.png) 984 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rds/hydrogen", 3 | "type": "library", 4 | "license": "MIT", 5 | "description": "More faster and convenient Doctrine ORM abstraction layer", 6 | "keywords": [ 7 | "collections", 8 | "repository", 9 | "doctrine", 10 | "orm", 11 | "optimisation", 12 | "relations", 13 | "abstraction" 14 | ], 15 | "support": { 16 | "issues": "https://github.com/rambler-digital-solutions/hydrogen/issues", 17 | "source": "https://github.com/rambler-digital-solutions/hydrogen" 18 | }, 19 | "authors": [ 20 | { 21 | "name": "Kirill Nesmeyanov", 22 | "email": "k.nesmeyanov@rambler-co.ru" 23 | } 24 | ], 25 | "require": { 26 | "php": ">=7.1", 27 | "doctrine/orm": "~2.5", 28 | "illuminate/support": ">=5.5" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "RDS\\Hydrogen\\": "src/" 33 | }, 34 | "files": [ 35 | "src/helpers.php" 36 | ] 37 | }, 38 | "require-dev": { 39 | "phpunit/phpunit": "~6.1", 40 | "fzaninotto/faker": "~1.8" 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "RDS\\Hydrogen\\Tests\\": "tests/" 45 | } 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true 52 | } 53 | -------------------------------------------------------------------------------- /src/Bootstrap.php: -------------------------------------------------------------------------------- 1 | RawFunction::class, 28 | 'FIELD' => FieldFunction::class, 29 | ]; 30 | 31 | /** 32 | * @param EntityManagerInterface $em 33 | * @return void 34 | */ 35 | public function register(EntityManagerInterface $em): void 36 | { 37 | $this->registerDQLFunctions($em->getConfiguration()); 38 | } 39 | 40 | /** 41 | * @param Configuration $config 42 | * @return void 43 | */ 44 | private function registerDQLFunctions(Configuration $config): void 45 | { 46 | foreach (self::DQL_FUNCTIONS as $name => $fn) { 47 | if (! $config->getCustomStringFunction($name)) { 48 | $config->addCustomStringFunction($name, $fn); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | inner = BaseCollection::wrap($elements); 40 | 41 | parent::__construct($this->inner->toArray()); 42 | 43 | $this->exportProxies(); 44 | } 45 | 46 | /** 47 | * @return void 48 | */ 49 | private function exportProxies(): void 50 | { 51 | if (static::$proxies === null) { 52 | $class = new \ReflectionClass($this->inner); 53 | $property = $class->getProperty('proxies'); 54 | $property->setAccessible(true); 55 | 56 | static::$proxies = $property->getValue(); 57 | } 58 | } 59 | 60 | /** 61 | * @param string $name 62 | * @param array $arguments 63 | * @return mixed 64 | * @throws \BadMethodCallException 65 | */ 66 | public static function __callStatic(string $name, array $arguments = []) 67 | { 68 | if (\method_exists(BaseCollection::class, $name)) { 69 | $result = BaseCollection::$name(...$arguments); 70 | 71 | if ($result instanceof BaseCollection) { 72 | return new static($result->toArray()); 73 | } 74 | 75 | return $result; 76 | } 77 | 78 | $error = \sprintf('Call to undefined method %s::%s', static::class, $name); 79 | throw new \BadMethodCallException($error); 80 | } 81 | 82 | /** 83 | * Wrap the given value in a collection if applicable. 84 | * 85 | * @param mixed $value 86 | * @return static 87 | */ 88 | public static function wrap($value): self 89 | { 90 | switch (true) { 91 | case $value instanceof self: 92 | return new static($value); 93 | 94 | case $value instanceof BaseCollection: 95 | return new static($value); 96 | 97 | default: 98 | return new static(Arr::wrap($value)); 99 | } 100 | } 101 | 102 | /** 103 | * @param string $name 104 | * @param array $arguments 105 | * @return mixed 106 | * @throws \BadMethodCallException 107 | */ 108 | public function __call(string $name, array $arguments = []) 109 | { 110 | if (\method_exists($this->inner, $name)) { 111 | $result = $this->inner->$name(...$arguments); 112 | 113 | if ($result instanceof BaseCollection) { 114 | return new static($result->toArray()); 115 | } 116 | 117 | return $result; 118 | } 119 | 120 | $error = \sprintf('Call to undefined method %s::%s', static::class, $name); 121 | throw new \BadMethodCallException($error); 122 | } 123 | 124 | /** 125 | * @param string $key 126 | * @return HigherOrderCollectionProxy 127 | * @throws \InvalidArgumentException 128 | */ 129 | public function __get(string $key): HigherOrderCollectionProxy 130 | { 131 | if (! \in_array($key, static::$proxies, true)) { 132 | $error = "Property [{$key}] does not exist on this collection instance."; 133 | throw new \InvalidArgumentException($error); 134 | } 135 | 136 | return new HigherOrderCollectionProxy($this, $key); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Criteria/Common/Field.php: -------------------------------------------------------------------------------- 1 | 0); 51 | 52 | $this->analyseAndFill($query); 53 | 54 | if (\count($this->chunks) === 0) { 55 | $this->prefixed = false; 56 | } 57 | } 58 | 59 | /** 60 | * @param string $query 61 | * @return void 62 | */ 63 | private function analyseAndFill(string $query): void 64 | { 65 | $analyzed = $this->analyse(new Lexer($query)); 66 | $haystack = 0; 67 | 68 | foreach ($analyzed as $chunk) { 69 | $this->chunks[] = \ltrim($chunk, ':'); 70 | $haystack += \strlen($chunk) + 1; 71 | } 72 | 73 | $before = \substr($query, 0, $analyzed->getReturn()); 74 | $after = \substr($query, $analyzed->getReturn() + \max(0, $haystack - 1)); 75 | 76 | $this->wrapper = $before . '%s' . $after; 77 | } 78 | 79 | /** 80 | * @param Lexer $lexer 81 | * @return \Generator|string[] 82 | */ 83 | private function analyse(Lexer $lexer): \Generator 84 | { 85 | [$offset, $keep] = [null, true]; 86 | 87 | foreach ($this->lex($lexer) as $token => $lookahead) { 88 | switch ($token['type']) { 89 | case Lexer::T_OPEN_PARENTHESIS: 90 | $keep = true; 91 | break; 92 | 93 | case Lexer::T_INPUT_PARAMETER: 94 | $this->prefixed = false; 95 | 96 | case Lexer::T_IDENTIFIER: 97 | if ($lookahead['type'] === Lexer::T_OPEN_PARENTHESIS) { 98 | $keep = false; 99 | } 100 | 101 | if ($keep) { 102 | if ($offset === null) { 103 | $offset = $token['position']; 104 | } 105 | $keep = false; 106 | yield $token['value']; 107 | } 108 | 109 | break; 110 | 111 | case Lexer::T_DOT: 112 | $keep = true; 113 | break; 114 | 115 | default: 116 | $keep = false; 117 | } 118 | } 119 | 120 | return (int)$offset; 121 | } 122 | 123 | /** 124 | * @param Lexer $lexer 125 | * @return \Generator 126 | */ 127 | private function lex(Lexer $lexer): \Generator 128 | { 129 | while ($lexer->moveNext()) { 130 | if ($lexer->token) { 131 | yield $lexer->token => $lexer->lookahead; 132 | } 133 | } 134 | 135 | yield $lexer->token => $lexer->lookahead ?? ['type' => null, 'value' => null]; 136 | } 137 | 138 | /** 139 | * @param string $query 140 | * @return Field 141 | */ 142 | public static function new(string $query): self 143 | { 144 | return new static($query); 145 | } 146 | 147 | /** 148 | * @return string 149 | */ 150 | public function getName(): string 151 | { 152 | return \implode(self::DEEP_DELIMITER, $this->chunks); 153 | } 154 | 155 | /** 156 | * @param string|null $alias 157 | * @return string 158 | */ 159 | public function toString(string $alias = null): string 160 | { 161 | $value = $alias && $this->prefixed 162 | ? \implode('.', [$alias, $this->getName()]) 163 | : $this->getName(); 164 | 165 | return \sprintf($this->wrapper, $value); 166 | } 167 | 168 | /** 169 | * @return bool 170 | */ 171 | public function isPrefixed(): bool 172 | { 173 | return $this->prefixed; 174 | } 175 | 176 | /** 177 | * @return string 178 | */ 179 | public function __toString(): string 180 | { 181 | return $this->toString(); 182 | } 183 | 184 | /** 185 | * @return iterable|Field[] 186 | */ 187 | public function getIterator(): iterable 188 | { 189 | $lastOne = \count($this->chunks) - 1; 190 | 191 | foreach ($this->chunks as $i => $chunk) { 192 | $clone = clone $this; 193 | $clone->chunks = [$chunk]; 194 | 195 | yield $lastOne === $i => $clone; 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Criteria/Common/FieldInterface.php: -------------------------------------------------------------------------------- 1 | query = $query; 32 | } 33 | 34 | /** 35 | * @param string $name 36 | * @return Field 37 | */ 38 | protected function field(string $name): Field 39 | { 40 | return new Field($name); 41 | } 42 | 43 | /** 44 | * @param Query $query 45 | * @return CriterionInterface 46 | */ 47 | public function attach(Query $query): CriterionInterface 48 | { 49 | $this->query = $query; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * @param Query $query 56 | * @return bool 57 | */ 58 | public function isAttachedTo(Query $query): bool 59 | { 60 | return $this->query === $query; 61 | } 62 | 63 | /** 64 | * @return bool 65 | */ 66 | public function isAttached(): bool 67 | { 68 | return $this->query !== null; 69 | } 70 | 71 | /** 72 | * @return Query 73 | */ 74 | public function getQuery(): Query 75 | { 76 | return $this->query; 77 | } 78 | 79 | /** 80 | * @return string 81 | */ 82 | public function getQueryAlias(): string 83 | { 84 | return $this->query->getAlias(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Criteria/CriterionInterface.php: -------------------------------------------------------------------------------- 1 | field = new Field($field); 35 | } 36 | 37 | /** 38 | * @return Field 39 | */ 40 | public function getField(): Field 41 | { 42 | return $this->field; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Criteria/Having.php: -------------------------------------------------------------------------------- 1 | relation = $this->field($relation); 52 | $this->inner = $inner; 53 | $this->type = $type; 54 | } 55 | 56 | /** 57 | * @return Field 58 | */ 59 | public function getRelation(): Field 60 | { 61 | return $this->relation; 62 | } 63 | 64 | /** 65 | * @return int 66 | */ 67 | public function getType(): int 68 | { 69 | return $this->type; 70 | } 71 | 72 | /** 73 | * @return bool 74 | */ 75 | public function hasJoinQuery(): bool 76 | { 77 | return $this->inner !== null; 78 | } 79 | 80 | /** 81 | * @param Query|null $query 82 | * @return Query 83 | */ 84 | public function getJoinQuery(Query $query = null): Query 85 | { 86 | $related = $query ?? $this->query->create(); 87 | 88 | if ($this->inner) { 89 | ($this->inner)($related); 90 | } 91 | 92 | return $related; 93 | } 94 | 95 | /** 96 | * @param ProcessorInterface $processor 97 | * @return \Generator|array 98 | */ 99 | public function getRelations(ProcessorInterface $processor): \Generator 100 | { 101 | $parent = $processor->getMetadata(); 102 | 103 | foreach ($this->getRelation() as $isLast => $relation) { 104 | yield $isLast => $from = $parent->associationMappings[$relation->getName()]; 105 | 106 | $parent = $processor->getProcessor($from['targetEntity'])->getMetadata(); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Criteria/Limit.php: -------------------------------------------------------------------------------- 1 | limit = $limit; 35 | } 36 | 37 | /** 38 | * @return int 39 | */ 40 | public function getLimit(): int 41 | { 42 | return $this->limit; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Criteria/Offset.php: -------------------------------------------------------------------------------- 1 | offset = $offset; 35 | } 36 | 37 | /** 38 | * @return int 39 | */ 40 | public function getOffset(): int 41 | { 42 | return $this->offset; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Criteria/OrderBy.php: -------------------------------------------------------------------------------- 1 | field = $this->field($field); 44 | $this->asc = $asc; 45 | } 46 | 47 | /** 48 | * @return Field 49 | */ 50 | public function getField(): Field 51 | { 52 | return $this->field; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getDirection(): string 59 | { 60 | return $this->asc ? static::ASC : static::DESC; 61 | } 62 | 63 | /** 64 | * @return bool 65 | */ 66 | public function isAsc(): bool 67 | { 68 | return $this->asc; 69 | } 70 | 71 | /** 72 | * @return bool 73 | */ 74 | public function isDesc(): bool 75 | { 76 | return ! $this->asc; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Criteria/Relation.php: -------------------------------------------------------------------------------- 1 | relation = $this->field($relation); 41 | $this->inner = $inner; 42 | } 43 | 44 | /** 45 | * @return Field 46 | */ 47 | public function getRelation(): Field 48 | { 49 | return $this->relation; 50 | } 51 | 52 | /** 53 | * @return Query 54 | */ 55 | public function getRelatedQuery(): Query 56 | { 57 | $related = $this->query->create(); 58 | 59 | if ($this->inner) { 60 | ($this->inner)($related); 61 | } 62 | 63 | return $related; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Criteria/Selection.php: -------------------------------------------------------------------------------- 1 | field = $this->field($field); 41 | $this->as = $alias; 42 | } 43 | 44 | /** 45 | * @return bool 46 | */ 47 | public function hasAlias(): bool 48 | { 49 | return $this->as !== null; 50 | } 51 | 52 | /** 53 | * @return null|string 54 | */ 55 | public function getAlias(): ?string 56 | { 57 | return $this->as; 58 | } 59 | 60 | /** 61 | * @return Field 62 | */ 63 | public function getField(): Field 64 | { 65 | return $this->field; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Criteria/Where.php: -------------------------------------------------------------------------------- 1 | field = $this->field($field); 55 | $this->value = $this->normalizeValue($value); 56 | $this->operator = $this->normalizeOperator(new Operator($operator), $this->value); 57 | $this->and = $and; 58 | } 59 | 60 | /** 61 | * @param mixed $value 62 | * @return array|mixed 63 | */ 64 | private function normalizeValue($value) 65 | { 66 | switch (true) { 67 | case $value instanceof Arrayable: 68 | return $value->toArray(); 69 | 70 | case $value instanceof \Traversable: 71 | return \iterator_to_array($value); 72 | 73 | case \is_object($value) && \method_exists($value, '__toString'): 74 | return (string)$value; 75 | } 76 | 77 | return $value; 78 | } 79 | 80 | /** 81 | * @param Operator $operator 82 | * @param mixed $value 83 | * @return Operator 84 | */ 85 | private function normalizeOperator(Operator $operator, $value): Operator 86 | { 87 | if (\is_array($value) && $operator->is(Operator::EQ)) { 88 | return $operator->changeTo(Operator::IN); 89 | } 90 | 91 | if (\is_array($value) && $operator->is(Operator::NEQ)) { 92 | return $operator->changeTo(Operator::NOT_IN); 93 | } 94 | 95 | return $operator; 96 | } 97 | 98 | /** 99 | * @param mixed $operator 100 | * @param null $value 101 | * @return array 102 | */ 103 | public static function completeMissingParameters($operator, $value = null): array 104 | { 105 | if ($value === null) { 106 | [$value, $operator] = [$operator, Operator::EQ]; 107 | } 108 | 109 | return [$operator, $value]; 110 | } 111 | 112 | /** 113 | * @return Field 114 | */ 115 | public function getField(): Field 116 | { 117 | return $this->field; 118 | } 119 | 120 | /** 121 | * @return Operator 122 | */ 123 | public function getOperator(): Operator 124 | { 125 | return $this->operator; 126 | } 127 | 128 | /** 129 | * @return mixed 130 | */ 131 | public function getValue() 132 | { 133 | return $this->value; 134 | } 135 | 136 | /** 137 | * @return bool 138 | */ 139 | public function isAnd(): bool 140 | { 141 | return $this->and; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Criteria/Where/Operator.php: -------------------------------------------------------------------------------- 1 | '; 21 | public const GT = '>'; 22 | public const GTE = '>='; 23 | public const LT = '<'; 24 | public const LTE = '<='; 25 | 26 | // X IN (...) 27 | public const IN = 'IN'; 28 | public const NOT_IN = 'NOT IN'; 29 | 30 | // LIKE 31 | public const LIKE = 'LIKE'; 32 | public const NOT_LIKE = 'NOT LIKE'; 33 | 34 | // BETWEEN 35 | public const BTW = 'BETWEEN'; 36 | public const NOT_BTW = 'NOT BETWEEN'; 37 | 38 | /** 39 | * Mappings 40 | * 41 | * Transform the given format into normal operator format. 42 | */ 43 | private const OPERATOR_MAPPINGS = [ 44 | '==' => self::EQ, 45 | 'IS' => self::EQ, 46 | '!=' => self::NEQ, 47 | 'NOTIS' => self::NEQ, 48 | '!IN' => self::NOT_IN, 49 | '~' => self::LIKE, 50 | '!LIKE' => self::NOT_LIKE, 51 | '!~' => self::NOT_LIKE, 52 | '..' => self::BTW, 53 | '...' => self::BTW, 54 | '!BETWEEN' => self::NOT_BTW, 55 | '!..' => self::NOT_BTW, 56 | '!...' => self::NOT_BTW, 57 | ]; 58 | 59 | /** 60 | * @var string 61 | */ 62 | private $operator; 63 | 64 | /** 65 | * Operator constructor. 66 | * @param string $operator 67 | */ 68 | public function __construct(string $operator) 69 | { 70 | $this->operator = $this->normalize($operator); 71 | } 72 | 73 | /** 74 | * @param string $operator 75 | * @return string 76 | */ 77 | private function normalize(string $operator): string 78 | { 79 | $upper = Str::upper($operator); 80 | 81 | $operator = \str_replace(' ', '', $upper); 82 | 83 | return self::OPERATOR_MAPPINGS[$operator] ?? $upper; 84 | } 85 | 86 | /** 87 | * @param string $operator 88 | * @return Operator 89 | */ 90 | public static function new(string $operator): self 91 | { 92 | return new static($operator); 93 | } 94 | 95 | /** 96 | * @param string $operator 97 | * @return Operator 98 | */ 99 | public function changeTo(string $operator): self 100 | { 101 | $this->operator = $operator; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * @return string 108 | */ 109 | public function toString(): string 110 | { 111 | return $this->operator; 112 | } 113 | 114 | /** 115 | * @return string 116 | */ 117 | public function __toString(): string 118 | { 119 | return $this->operator; 120 | } 121 | 122 | /** 123 | * @param string $operator 124 | * @return bool 125 | */ 126 | public function is(string $operator): bool 127 | { 128 | return $this->operator === Str::upper($operator, 'UTF-8'); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Criteria/WhereGroup.php: -------------------------------------------------------------------------------- 1 | then = $then; 40 | $this->conjunction = $conjunction; 41 | } 42 | 43 | /** 44 | * @return bool 45 | */ 46 | public function isAnd(): bool 47 | { 48 | return $this->conjunction; 49 | } 50 | 51 | /** 52 | * @return Query 53 | */ 54 | public function getQuery(): Query 55 | { 56 | $query = $this->query->create() 57 | ->withAlias($this->query->getAlias()); 58 | 59 | ($this->then)($query); 60 | 61 | return $query; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Fun/FieldFunction.php: -------------------------------------------------------------------------------- 1 | match(Lexer::T_IDENTIFIER); 44 | $parser->match(Lexer::T_OPEN_PARENTHESIS); 45 | $this->table = $parser->StringPrimary(); 46 | $parser->match(Lexer::T_COMMA); 47 | $this->alias = $parser->StringPrimary(); 48 | $parser->match(Lexer::T_COMMA); 49 | $this->field = $parser->StringPrimary(); 50 | $parser->match(Lexer::T_CLOSE_PARENTHESIS); 51 | } 52 | 53 | /** 54 | * @param SqlWalker $sqlWalker 55 | * @return string 56 | */ 57 | public function getSql(SqlWalker $sqlWalker): string 58 | { 59 | $alias = $sqlWalker->getSQLTableAlias($this->table->value, $this->alias->value); 60 | 61 | return $alias . '.' . $this->field->value; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Fun/RawFunction.php: -------------------------------------------------------------------------------- 1 | match(Lexer::T_IDENTIFIER); 34 | $parser->match(Lexer::T_OPEN_PARENTHESIS); 35 | $this->expr = $parser->StringPrimary(); 36 | $parser->match(Lexer::T_CLOSE_PARENTHESIS); 37 | } 38 | 39 | /** 40 | * @param SqlWalker $sqlWalker 41 | * @return string 42 | */ 43 | public function getSql(SqlWalker $sqlWalker): string 44 | { 45 | return $this->expr->value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/HighOrderMessaging/HigherOrderCollectionProxy.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 44 | $this->method = $method; 45 | } 46 | 47 | /** 48 | * @param string $property 49 | * @return Collection|mixed 50 | */ 51 | public function __get(string $property) 52 | { 53 | return $this->collection->{$this->method}(function($item) use ($property) { 54 | if ($this->hasProperty($item, $property)) { 55 | return $item->$property; 56 | } 57 | 58 | if ($this->isArrayable($item)) { 59 | return $item[$property]; 60 | } 61 | 62 | if ($this->hasMethod($item, $property)) { 63 | return $item->$property(); 64 | } 65 | 66 | if (\function_exists($property)) { 67 | return $property($item); 68 | } 69 | 70 | $snake = Str::snake($property); 71 | 72 | if (\function_exists($snake)) { 73 | return $snake($item); 74 | } 75 | }); 76 | } 77 | 78 | /** 79 | * @param string $method 80 | * @param array $arguments 81 | * @return Collection|mixed 82 | */ 83 | public function __call(string $method, array $arguments = []) 84 | { 85 | return $this->collection->{$this->method}(function($item) use ($method, $arguments) { 86 | if ($this->hasMethod($item, $method)) { 87 | return $item->$method(...$arguments); 88 | } 89 | 90 | if ($this->hasCallableProperty($item, $method)) { 91 | return ($item->$method)(...$arguments); 92 | } 93 | 94 | if ($this->hasCallableKey($item, $method)) { 95 | return $item[$method](...$arguments); 96 | } 97 | 98 | if (\function_exists($method)) { 99 | return $method(...$this->pack($item, $arguments)); 100 | } 101 | 102 | $snake = Str::snake($method); 103 | 104 | if (\function_exists(Str::snake($snake))) { 105 | return $snake(...$this->pack($item, $arguments)); 106 | } 107 | }); 108 | } 109 | 110 | /** 111 | * @param object $context 112 | * @return bool 113 | */ 114 | private function isArrayable($context): bool 115 | { 116 | return \is_array($context) || $context instanceof \ArrayAccess; 117 | } 118 | 119 | /** 120 | * @param object $context 121 | * @param string $key 122 | * @return bool 123 | */ 124 | private function hasCallableKey($context, string $key): bool 125 | { 126 | return $this->isArrayable($context) && \is_callable($context[$key] ?? null); 127 | } 128 | 129 | /** 130 | * @param object $context 131 | * @param string $property 132 | * @return bool 133 | */ 134 | private function hasProperty($context, string $property): bool 135 | { 136 | return \is_object($context) && ( 137 | \property_exists($context, $property) || 138 | \method_exists($context, '__get') 139 | ); 140 | } 141 | 142 | /** 143 | * @param object $context 144 | * @param string $property 145 | * @return bool 146 | */ 147 | private function hasCallableProperty($context, string $property): bool 148 | { 149 | return $this->hasProperty($context, $property) && \is_callable($context->$property); 150 | } 151 | 152 | /** 153 | * @param object $context 154 | * @param string $method 155 | * @return bool 156 | */ 157 | private function hasMethod($context, string $method): bool 158 | { 159 | return \is_object($context) && ( 160 | \method_exists($context, $method) || 161 | \method_exists($context, '__call') 162 | ); 163 | } 164 | 165 | /** 166 | * @param object $context 167 | * @param array $parameters 168 | * @return array 169 | */ 170 | private function pack($context, array $parameters): array 171 | { 172 | $result = []; 173 | 174 | foreach ($parameters as $parameter) { 175 | $result[] = $parameter === self::PATTERN ? $context : $parameter; 176 | } 177 | 178 | return $result; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Hydrogen.php: -------------------------------------------------------------------------------- 1 | processor === null) { 34 | $this->processor = new DatabaseProcessor($this, $this->getEntityManager()); 35 | } 36 | 37 | return $this->processor; 38 | } 39 | 40 | /** 41 | * @return EntityManager 42 | */ 43 | public function getEntityManager(): EntityManager 44 | { 45 | return parent::getEntityManager(); 46 | } 47 | 48 | /** 49 | * @return Query|$this 50 | * @throws \LogicException 51 | */ 52 | public function query(): Query 53 | { 54 | if (! $this instanceof EntityRepository) { 55 | $error = 'Could not use %s under non-repository class, but %s given'; 56 | throw new \LogicException(\sprintf($error, Hydrogen::class, static::class)); 57 | } 58 | 59 | return Query::new()->from($this); 60 | } 61 | 62 | /** 63 | * @param string $name 64 | * @return null|Query 65 | * @throws \LogicException 66 | */ 67 | public function __get(string $name) 68 | { 69 | switch ($name) { 70 | case 'query': 71 | return $this->query(); 72 | } 73 | 74 | 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Processor/BuilderInterface.php: -------------------------------------------------------------------------------- 1 | DatabaseProcessor\GroupByBuilder::class, 28 | Criteria\Having::class => DatabaseProcessor\HavingBuilder::class, 29 | Criteria\HavingGroup::class => DatabaseProcessor\HavingGroupBuilder::class, 30 | Criteria\Join::class => DatabaseProcessor\JoinBuilder::class, 31 | Criteria\Limit::class => DatabaseProcessor\LimitBuilder::class, 32 | Criteria\Offset::class => DatabaseProcessor\OffsetBuilder::class, 33 | Criteria\OrderBy::class => DatabaseProcessor\OrderByBuilder::class, 34 | Criteria\Relation::class => DatabaseProcessor\RelationBuilder::class, 35 | Criteria\Selection::class => DatabaseProcessor\SelectBuilder::class, 36 | Criteria\Where::class => DatabaseProcessor\WhereBuilder::class, 37 | Criteria\WhereGroup::class => DatabaseProcessor\GroupBuilder::class, 38 | ]; 39 | 40 | /** 41 | * @param Query $query 42 | * @param string $field 43 | * @return mixed 44 | */ 45 | public function getScalarResult(Query $query, string $field) 46 | { 47 | $query->from($this->repository); 48 | 49 | /** @var QueryBuilder $builder */ 50 | [$deferred, $builder] = $this->await($this->createQueryBuilder($query)); 51 | 52 | return $builder->getQuery()->getSingleScalarResult(); 53 | } 54 | 55 | /** 56 | * @param Query $query 57 | * @return string 58 | */ 59 | public function dump(Query $query): string 60 | { 61 | $query->from($this->repository); 62 | 63 | /** @var QueryBuilder $builder */ 64 | [$deferred, $builder] = $this->await($this->createQueryBuilder($query)); 65 | 66 | return $builder->getQuery()->getDQL(); 67 | } 68 | 69 | /** 70 | * @param Query $query 71 | * @return \Generator 72 | */ 73 | protected function createQueryBuilder(Query $query): \Generator 74 | { 75 | $builder = $this->em->createQueryBuilder(); 76 | $builder->from($query->getRepository()->getClassName(), $query->getAlias()); 77 | $builder->setCacheable(false); 78 | 79 | return $this->fillQueryBuilder($builder, $query); 80 | } 81 | 82 | /** 83 | * @param QueryBuilder $builder 84 | * @param Query $query 85 | * @return \Generator 86 | */ 87 | protected function fillQueryBuilder(QueryBuilder $builder, Query $query): \Generator 88 | { 89 | /** 90 | * @var \Generator $context 91 | * @var CriterionInterface $criterion 92 | */ 93 | foreach ($this->bypass($builder, $query) as $criterion => $context) { 94 | while ($context->valid()) { 95 | [$key, $value] = [$context->key(), $context->current()]; 96 | 97 | switch (true) { 98 | case $key instanceof Field: 99 | $context->send($placeholder = $query->createPlaceholder($key->toString())); 100 | $builder->setParameter($placeholder, $value); 101 | continue 2; 102 | 103 | case $value instanceof Field: 104 | $context->send($value->toString($criterion->getQueryAlias())); 105 | continue 2; 106 | 107 | case $value instanceof Query: 108 | $context->send($query->attach($value)); 109 | continue 2; 110 | 111 | default: 112 | $result = (yield $key => $value); 113 | 114 | if ($result === null) { 115 | $stmt = \is_object($value) ? \get_class($value) : \gettype($value); 116 | $error = 'Unrecognized coroutine\'s return statement: ' . $stmt; 117 | $context->throw(new \InvalidArgumentException($error)); 118 | } 119 | 120 | $context->send($result); 121 | } 122 | } 123 | } 124 | 125 | return $builder; 126 | } 127 | 128 | /** 129 | * @param Query $query 130 | * @param string ...$fields 131 | * @return iterable 132 | */ 133 | public function getResult(Query $query, string ...$fields): iterable 134 | { 135 | $query->from($this->repository); 136 | 137 | if (! $query->has(Criteria\Selection::class)) { 138 | $query->select(':' . $query->getAlias()); 139 | } 140 | 141 | /** 142 | * @var QueryBuilder $builder 143 | * @var Queue $deferred 144 | */ 145 | [$deferred, $builder] = $this->await($this->createQueryBuilder($query)); 146 | 147 | return \count($fields) > 0 148 | ? $this->executeFetchFields($builder, $fields) 149 | : $this->executeFetchData($builder, $deferred); 150 | } 151 | 152 | /** 153 | * @param QueryBuilder $builder 154 | * @param array $fields 155 | * @return array 156 | */ 157 | private function executeFetchFields(QueryBuilder $builder, array $fields): array 158 | { 159 | $result = []; 160 | 161 | foreach ($builder->getQuery()->getArrayResult() as $record) { 162 | $result[] = \array_merge(\array_only($record, $fields), \array_only($record[0] ?? [], $fields)); 163 | } 164 | 165 | return $result; 166 | } 167 | 168 | /** 169 | * @param QueryBuilder $builder 170 | * @param Queue $deferred 171 | * @return array 172 | */ 173 | private function executeFetchData(QueryBuilder $builder, Queue $deferred): array 174 | { 175 | $query = $builder->getQuery(); 176 | 177 | $deferred->invoke($result = $query->getResult()); 178 | 179 | return $result; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/Builder.php: -------------------------------------------------------------------------------- 1 | processor = $processor; 40 | $this->query = $query; 41 | } 42 | 43 | /** 44 | * @param string $entity 45 | * @param Query $query 46 | * @return iterable 47 | */ 48 | protected function execute(string $entity, Query $query): iterable 49 | { 50 | return $this->processor->getProcessor($entity)->getResult($query); 51 | } 52 | 53 | /** 54 | * @return \Generator 55 | */ 56 | protected function nothing(): \Generator 57 | { 58 | if (false) { 59 | yield; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/Common/Expression.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 46 | $this->operator = $operator; 47 | $this->value = $value; 48 | } 49 | 50 | /** 51 | * @param Field $field 52 | * @return Expr\Comparison|Expr\Func|string|\Generator 53 | */ 54 | public function create(Field $field): \Generator 55 | { 56 | $expr = $this->builder->expr(); 57 | $operator = $this->operator->toString(); 58 | 59 | /** 60 | * Expr: 61 | * - "X IS NULL" 62 | * - "X IS NOT NULL" 63 | */ 64 | if ($this->value === null) { 65 | switch ($operator) { 66 | case Operator::EQ: 67 | return $expr->isNull(yield $field); 68 | 69 | case Operator::NEQ: 70 | return $expr->isNotNull(yield $field); 71 | } 72 | } 73 | 74 | switch ($operator) { 75 | case Operator::EQ: 76 | return $expr->eq( 77 | yield $field, 78 | yield $field => $this->value 79 | ); 80 | 81 | case Operator::NEQ: 82 | return $expr->neq( 83 | yield $field, 84 | yield $field => $this->value 85 | ); 86 | 87 | case Operator::GT: 88 | return $expr->gt( 89 | yield $field, 90 | yield $field => $this->value 91 | ); 92 | 93 | case Operator::LT: 94 | return $expr->lt( 95 | yield $field, 96 | yield $field => $this->value 97 | ); 98 | 99 | case Operator::GTE: 100 | return $expr->gte( 101 | yield $field, 102 | yield $field => $this->value 103 | ); 104 | 105 | case Operator::LTE: 106 | return $expr->lte( 107 | yield $field, 108 | yield $field => $this->value 109 | ); 110 | 111 | case Operator::IN: 112 | return $expr->in( 113 | yield $field, 114 | yield $field => $this->value 115 | ); 116 | 117 | case Operator::NOT_IN: 118 | return $expr->notIn( 119 | yield $field, 120 | yield $field => $this->value 121 | ); 122 | 123 | case Operator::LIKE: 124 | return $expr->like( 125 | yield $field, 126 | yield $field => $this->value 127 | ); 128 | case Operator::NOT_LIKE: 129 | return $expr->notLike( 130 | yield $field, 131 | yield $field => $this->value 132 | ); 133 | 134 | case Operator::BTW: 135 | return $expr->between( 136 | yield $field, 137 | yield $field => $this->value[0] ?? null, 138 | yield $field => $this->value[1] ?? null 139 | ); 140 | 141 | case Operator::NOT_BTW: 142 | return \vsprintf('%s NOT BETWEEN %s AND %s', [ 143 | yield $field, 144 | yield $field => $this->value[0] ?? null, 145 | yield $field => $this->value[1] ?? null, 146 | ]); 147 | } 148 | 149 | $error = \sprintf('Unexpected "%s" operator type', $operator); 150 | throw new \InvalidArgumentException($error); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/GroupBuilder.php: -------------------------------------------------------------------------------- 1 | 'applyWhere', 30 | WhereGroup::class => 'applyGroup', 31 | ]; 32 | 33 | /** 34 | * @param QueryBuilder $builder 35 | * @param Andx $context 36 | * @param WhereGroup $group 37 | * @return \Generator 38 | */ 39 | protected function applyGroup(QueryBuilder $builder, Andx $context, WhereGroup $group): \Generator 40 | { 41 | return $this->apply($builder, $group); 42 | } 43 | 44 | /** 45 | * @param QueryBuilder $builder 46 | * @param CriterionInterface|WhereGroup $group 47 | * @return iterable|null 48 | */ 49 | public function apply($builder, CriterionInterface $group): ?iterable 50 | { 51 | $expression = $builder->expr()->andX(); 52 | 53 | foreach ($this->getInnerSelections($group) as $criterion => $fn) { 54 | yield from $fn($builder, $expression, $criterion); 55 | } 56 | 57 | return $group->isAnd() ? $builder->andWhere($expression) : $builder->orWhere($expression); 58 | } 59 | 60 | /** 61 | * @param WhereGroup $group 62 | * @return iterable|callable[] 63 | */ 64 | protected function getInnerSelections(WhereGroup $group): iterable 65 | { 66 | $query = $group->getQuery(); 67 | 68 | foreach ($query->getCriteria() as $criterion) { 69 | foreach (static::ALLOWED_INNER_TYPES as $typeOf => $fn) { 70 | if ($criterion instanceof $typeOf) { 71 | yield $criterion => [$this, $fn]; 72 | continue 2; 73 | } 74 | } 75 | 76 | $error = 'Groups not allowed for %s criterion'; 77 | throw new \LogicException(\sprintf($error, \get_class($criterion))); 78 | } 79 | } 80 | 81 | /** 82 | * @param QueryBuilder $builder 83 | * @param Andx $context 84 | * @param Where $where 85 | * @return \Generator 86 | */ 87 | protected function applyWhere(QueryBuilder $builder, Andx $context, Where $where): \Generator 88 | { 89 | $expression = new Expression($builder, $where->getOperator(), $where->getValue()); 90 | yield from $result = $expression->create($where->getField()); 91 | 92 | if ($where->isAnd()) { 93 | $context->add($result->getReturn()); 94 | } else { 95 | $builder->orWhere($result->getReturn()); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/GroupByBuilder.php: -------------------------------------------------------------------------------- 1 | addGroupBy(yield $groupBy->getField()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/HavingBuilder.php: -------------------------------------------------------------------------------- 1 | getOperator(), $having->getValue()); 30 | yield from $result = $expression->create($having->getField()); 31 | 32 | if ($having->isAnd()) { 33 | $builder->andHaving($result->getReturn()); 34 | } else { 35 | $builder->orHaving($result->getReturn()); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/HavingGroupBuilder.php: -------------------------------------------------------------------------------- 1 | 'applyWhere', 31 | Having::class => 'applyWhere', 32 | WhereGroup::class => 'applyGroup', 33 | ]; 34 | 35 | /** 36 | * @param QueryBuilder $builder 37 | * @param CriterionInterface|WhereGroup $group 38 | * @return iterable|null 39 | */ 40 | public function apply($builder, CriterionInterface $group): ?iterable 41 | { 42 | $expression = $builder->expr()->andX(); 43 | 44 | foreach ($this->getInnerSelections($group) as $criterion => $fn) { 45 | yield from $fn($builder, $expression, $criterion); 46 | } 47 | 48 | return $group->isAnd() ? $builder->andHaving($expression) : $builder->orHaving($expression); 49 | } 50 | 51 | /** 52 | * @param QueryBuilder $builder 53 | * @param Andx $context 54 | * @param Where $where 55 | * @return \Generator 56 | */ 57 | final protected function applyWhere(QueryBuilder $builder, Andx $context, Where $where): \Generator 58 | { 59 | $expression = new Expression($builder, $where->getOperator(), $where->getValue()); 60 | yield from $result = $expression->create($where->getField()); 61 | 62 | if ($where->isAnd()) { 63 | $context->add($result->getReturn()); 64 | } else { 65 | $builder->orHaving($result->getReturn()); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/JoinBuilder.php: -------------------------------------------------------------------------------- 1 | joinAll($builder, $join); 36 | 37 | if ($join->hasJoinQuery()) { 38 | $repository = $this->processor->getProcessor($entity)->getRepository(); 39 | 40 | $query = $this->query->create() 41 | ->from($repository) 42 | ->withAlias($alias); 43 | 44 | yield $join->getJoinQuery($query); 45 | } 46 | } 47 | 48 | /** 49 | * @param QueryBuilder $builder 50 | * @param Join $join 51 | * @return array 52 | */ 53 | private function joinAll(QueryBuilder $builder, Join $join): array 54 | { 55 | [$alias, $relation] = [$join->getQueryAlias(), []]; 56 | 57 | foreach ($join->getRelations($this->processor) as $isLast => $relation) { 58 | 59 | // Is the relation already loaded in current query execution 60 | $exists = $this->hasAlias($relation); 61 | 62 | // Resolve relation alias 63 | $relationAlias = $isLast && $join->hasJoinQuery() 64 | ? $this->getAlias($relation) 65 | : $this->getCachedAlias($relation); 66 | 67 | if (! $exists) { 68 | // Create join 69 | $relationField = Field::new($relation['fieldName'])->toString($alias); 70 | $this->join($builder, $join, $relationField, $relationAlias); 71 | 72 | // Add join to selection statement 73 | $builder->addSelect($relationAlias); 74 | } 75 | 76 | // Shift parent 77 | $alias = $relationAlias; 78 | } 79 | 80 | return [$relation['targetEntity'], $alias]; 81 | } 82 | 83 | /** 84 | * @param QueryBuilder $builder 85 | * @param Join $join 86 | * @param string $field 87 | * @param string $relationAlias 88 | * @return void 89 | */ 90 | private function join(QueryBuilder $builder, Join $join, string $field, string $relationAlias): void 91 | { 92 | switch ($join->getType()) { 93 | case Join::TYPE_JOIN: 94 | $builder->join($field, $relationAlias); 95 | break; 96 | 97 | case Join::TYPE_INNER_JOIN: 98 | $builder->innerJoin($field, $relationAlias); 99 | break; 100 | 101 | case Join::TYPE_LEFT_JOIN: 102 | $builder->leftJoin($field, $relationAlias); 103 | break; 104 | } 105 | } 106 | 107 | /** 108 | * @param array $relation 109 | * @return string 110 | */ 111 | private function getKey(array $relation): string 112 | { 113 | return $relation['sourceEntity'] . '_' . $relation['targetEntity']; 114 | } 115 | 116 | /** 117 | * @param array $relation 118 | * @return bool 119 | */ 120 | private function hasAlias(array $relation): bool 121 | { 122 | $key = $this->getKey($relation); 123 | 124 | return isset($this->relations[$key]); 125 | } 126 | 127 | /** 128 | * @param array $relation 129 | * @return string 130 | */ 131 | private function getCachedAlias(array $relation): string 132 | { 133 | $key = $this->getKey($relation); 134 | 135 | if (! isset($this->relations[$key])) { 136 | return $this->relations[$key] = 137 | $this->query->createAlias( 138 | $relation['sourceEntity'], 139 | $relation['targetEntity'] 140 | ); 141 | } 142 | 143 | return $this->relations[$key]; 144 | } 145 | 146 | /** 147 | * @param array $relation 148 | * @return string 149 | */ 150 | private function getAlias(array $relation): string 151 | { 152 | return $this->query->createAlias( 153 | $relation['sourceEntity'], 154 | $relation['targetEntity'] 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/LimitBuilder.php: -------------------------------------------------------------------------------- 1 | setMaxResults($limit->getLimit()); 29 | 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/OffsetBuilder.php: -------------------------------------------------------------------------------- 1 | setFirstResult($offset->getOffset()); 29 | 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/OrderByBuilder.php: -------------------------------------------------------------------------------- 1 | orderBy(yield $orderBy->getField(), $orderBy->getDirection()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/RelationBuilder.php: -------------------------------------------------------------------------------- 1 | getField(); 30 | 31 | if ($select->hasAlias()) { 32 | $selection .= ' AS ' . $select->getAlias(); 33 | } 34 | 35 | $builder->addSelect($selection); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Processor/DatabaseProcessor/WhereBuilder.php: -------------------------------------------------------------------------------- 1 | getOperator(), $where->getValue()); 32 | yield from $result = $expression->create($where->getField()); 33 | 34 | if ($where->isAnd()) { 35 | $builder->andWhere($result->getReturn()); 36 | } else { 37 | $builder->orWhere($result->getReturn()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Processor/Processor.php: -------------------------------------------------------------------------------- 1 | em = $em; 52 | $this->meta = $em->getClassMetadata($repository->getClassName()); 53 | $this->repository = $repository; 54 | 55 | \assert(\count(static::CRITERIA_MAPPINGS)); 56 | } 57 | 58 | /** 59 | * @return EntityManagerInterface 60 | */ 61 | public function getEntityManager(): EntityManagerInterface 62 | { 63 | return $this->em; 64 | } 65 | 66 | /** 67 | * @return EntityRepository|ObjectRepository 68 | */ 69 | public function getRepository(): ObjectRepository 70 | { 71 | return $this->repository; 72 | } 73 | 74 | /** 75 | * @return ClassMetadata 76 | */ 77 | public function getMetadata(): ClassMetadata 78 | { 79 | return $this->meta; 80 | } 81 | 82 | /** 83 | * @param mixed $context 84 | * @param Query $query 85 | * @return \Generator 86 | */ 87 | protected function bypass($context, Query $query): \Generator 88 | { 89 | foreach ($this->builders($query) as $criterion => $builder) { 90 | $result = $builder->apply($context, $criterion); 91 | 92 | if (\is_iterable($result)) { 93 | yield $criterion => $result; 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * @param \Generator $generator 100 | * @return array 101 | */ 102 | protected function await(\Generator $generator): array 103 | { 104 | $queue = new Queue(); 105 | 106 | while ($generator->valid()) { 107 | $value = $generator->current(); 108 | 109 | if ($value instanceof \Closure) { 110 | $queue->push($value); 111 | } 112 | 113 | $generator->next(); 114 | } 115 | 116 | return [$queue, $generator->getReturn()]; 117 | } 118 | 119 | /** 120 | * @param Query $query 121 | * @return \Generator|BuilderInterface[] 122 | */ 123 | private function builders(Query $query): \Generator 124 | { 125 | $context = []; 126 | 127 | foreach ($query->getCriteria() as $criterion) { 128 | $key = \get_class($criterion); 129 | 130 | yield $criterion => $context[$key] ?? $context[$key] = $this->getBuilder($query, $criterion); 131 | } 132 | 133 | unset($context); 134 | } 135 | 136 | /** 137 | * @param Query $query 138 | * @param CriterionInterface $criterion 139 | * @return BuilderInterface 140 | */ 141 | protected function getBuilder(Query $query, CriterionInterface $criterion): BuilderInterface 142 | { 143 | $processor = static::CRITERIA_MAPPINGS[\get_class($criterion)] ?? null; 144 | 145 | if ($processor === null) { 146 | $error = \vsprintf('%s processor does not support the "%s" criterion', [ 147 | \str_replace_last('Processor', '', \class_basename($this)), 148 | \class_basename($criterion), 149 | ]); 150 | 151 | throw new \InvalidArgumentException($error); 152 | } 153 | 154 | return new $processor($query, $this); 155 | } 156 | 157 | /** 158 | * @param string $entity 159 | * @return ProcessorInterface 160 | */ 161 | public function getProcessor(string $entity): ProcessorInterface 162 | { 163 | $repository = $this->em->getRepository($entity); 164 | 165 | if (\method_exists($repository, 'getProcessor')) { 166 | return $repository->getProcessor(); 167 | } 168 | 169 | return new static($repository, $this->em); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Processor/ProcessorInterface.php: -------------------------------------------------------------------------------- 1 | queue = new \SplQueue(); 28 | } 29 | 30 | /** 31 | * @param \Closure $deferred 32 | * @return Queue 33 | */ 34 | public function push(\Closure $deferred): Queue 35 | { 36 | $this->queue->push($deferred); 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * @return \Generator|\Closure[] 43 | */ 44 | public function getIterator(): \Generator 45 | { 46 | while ($this->queue->count()) { 47 | yield $this->queue->pop(); 48 | } 49 | } 50 | 51 | /** 52 | * @param mixed $value 53 | * @return void 54 | */ 55 | public function invoke($value): void 56 | { 57 | foreach ($this->queue as $callback) { 58 | $callback($value); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | from($repository); 88 | } 89 | } 90 | 91 | /** 92 | * Method for creating native DB queries or query parts. 93 | * 94 | * @param string $stmt 95 | * @return string 96 | */ 97 | public static function raw(string $stmt): string 98 | { 99 | return \sprintf("RAW('%s')", \addcslashes($stmt, "'")); 100 | } 101 | 102 | /** 103 | * The method checks for the presence of the required criterion inside the query. 104 | * 105 | * TODO Add callable argument support (like filter). 106 | * 107 | * @param string $criterion 108 | * @return bool 109 | */ 110 | public function has(string $criterion): bool 111 | { 112 | foreach ($this->criteria as $haystack) { 113 | if (\get_class($haystack) === $criterion) { 114 | return true; 115 | } 116 | } 117 | 118 | return false; 119 | } 120 | 121 | /** 122 | * Provides the ability to directly access methods without specifying parentheses. 123 | * 124 | * TODO 1) Add High Order Messaging for methods like `->field->where(23)` instead `->where('field', 23)` 125 | * TODO 2) Allow inner access `->embedded->field->where(23)` instead `->where('embedded.field', 23)` 126 | * 127 | * @param string $name 128 | * @return null 129 | */ 130 | public function __get(string $name) 131 | { 132 | if (\method_exists($this, $name)) { 133 | return $this->$name(); 134 | } 135 | 136 | return null; 137 | } 138 | 139 | /** 140 | * Creates the ability to directly access the table's column. 141 | * 142 | * @param string $name 143 | * @return string 144 | */ 145 | public function column(string $name): string 146 | { 147 | $name = \addcslashes($name, "'"); 148 | $table = $this->getMetadata()->getTableName(); 149 | 150 | return \sprintf("FIELD('%s', '%s', '%s')", $table, $this->getAlias(), $name); 151 | } 152 | 153 | /** 154 | * @internal For internal use only 155 | * @return ClassMetadata 156 | */ 157 | public function getMetadata(): ClassMetadata 158 | { 159 | return $this->getEntityManager()->getClassMetadata($this->getClassName()); 160 | } 161 | 162 | /** 163 | * @internal For internal use only 164 | * @return EntityManagerInterface 165 | */ 166 | public function getEntityManager(): EntityManagerInterface 167 | { 168 | return $this->getRepository()->getEntityManager(); 169 | } 170 | 171 | /** 172 | * @internal For internal use only 173 | * @return string 174 | */ 175 | public function getClassName(): string 176 | { 177 | return $this->getRepository()->getClassName(); 178 | } 179 | 180 | /** 181 | * @param string $method 182 | * @param array $parameters 183 | * @return mixed|$this|Query 184 | */ 185 | public function __call(string $method, array $parameters) 186 | { 187 | if ($result = $this->callScopes($method, $parameters)) { 188 | return $result; 189 | } 190 | 191 | return $this->__macroableCall($method, $parameters); 192 | } 193 | 194 | /** 195 | * @param string $method 196 | * @param array $parameters 197 | * @return null|Query|mixed 198 | */ 199 | private function callScopes(string $method, array $parameters = []) 200 | { 201 | foreach ($this->scopes as $scope) { 202 | if (\method_exists($scope, $method)) { 203 | /** @var Query $query */ 204 | $query = \is_object($scope) ? $scope->$method(...$parameters) : $scope::$method(...$parameters); 205 | 206 | if ($query instanceof self) { 207 | return $this->merge($query->clone()); 208 | } 209 | 210 | return $query; 211 | } 212 | } 213 | 214 | return null; 215 | } 216 | 217 | /** 218 | * Copies a set of Criteria from the child query to the parent. 219 | * 220 | * @param Query $query 221 | * @return Query 222 | */ 223 | public function merge(Query $query): Query 224 | { 225 | foreach ($query->getCriteria() as $criterion) { 226 | $criterion->attach($this); 227 | } 228 | 229 | return $this->attach($query); 230 | } 231 | 232 | /** 233 | * Returns a list of selection criteria. 234 | * 235 | * @return \Generator|CriterionInterface[] 236 | */ 237 | public function getCriteria(): \Generator 238 | { 239 | yield from $this->criteria; 240 | } 241 | 242 | /** 243 | * @param Query $query 244 | * @return Query 245 | */ 246 | public function attach(Query $query): Query 247 | { 248 | foreach ($query->getCriteria() as $criterion) { 249 | $this->add($criterion); 250 | } 251 | 252 | return $this; 253 | } 254 | 255 | /** 256 | * Creates a new query (alias to the constructor). 257 | * 258 | * @param CriterionInterface $criterion 259 | * @return Query|$this 260 | */ 261 | public function add(CriterionInterface $criterion): self 262 | { 263 | if (! $criterion->isAttached()) { 264 | $criterion->attach($this); 265 | } 266 | 267 | $this->criteria[] = $criterion; 268 | 269 | return $this; 270 | } 271 | 272 | /** 273 | * @return Query 274 | */ 275 | public function clone(): Query 276 | { 277 | $clone = $this->create(); 278 | 279 | foreach ($this->criteria as $criterion) { 280 | $criterion = clone $criterion; 281 | 282 | if ($criterion->isAttachedTo($this)) { 283 | $criterion->attach($clone); 284 | } 285 | 286 | $clone->add($criterion); 287 | } 288 | 289 | return $clone; 290 | } 291 | 292 | /** 293 | * Creates a new query using the current set of scopes. 294 | * 295 | * @return Query 296 | */ 297 | public function create(): Query 298 | { 299 | $query = static::new()->scope(...$this->getScopes()); 300 | 301 | if ($this->repository) { 302 | return $query->from($this->repository); 303 | } 304 | 305 | return $query; 306 | } 307 | 308 | /** 309 | * Adds the specified set of scopes (method groups) to the query. 310 | * 311 | * @param object|string ...$scopes 312 | * @return Query|$this 313 | */ 314 | public function scope(...$scopes): self 315 | { 316 | $this->scopes = \array_merge($this->scopes, $scopes); 317 | 318 | return $this; 319 | } 320 | 321 | /** 322 | * Creates a new query (alias to the constructor). 323 | * 324 | * @param ObjectRepository|null $repository 325 | * @return Query 326 | */ 327 | public static function new(ObjectRepository $repository = null): Query 328 | { 329 | return new static($repository); 330 | } 331 | 332 | /** 333 | * Returns a set of scopes for the specified query. 334 | * 335 | * @return array|ObjectRepository[] 336 | */ 337 | public function getScopes(): array 338 | { 339 | return $this->scopes; 340 | } 341 | 342 | /** 343 | * @return void 344 | * @throws \LogicException 345 | */ 346 | public function __clone() 347 | { 348 | $error = '%s not allowed. Use %s::clone() instead'; 349 | 350 | throw new \LogicException(\sprintf($error, __METHOD__, __CLASS__)); 351 | } 352 | 353 | /** 354 | * @param string|\Closure $filter 355 | * @return Query 356 | */ 357 | public function except($filter): Query 358 | { 359 | if (\is_string($filter) && ! \is_callable($filter)) { 360 | return $this->only(function (CriterionInterface $criterion) use ($filter): bool { 361 | return ! $criterion instanceof $filter; 362 | }); 363 | } 364 | 365 | return $this->only(function (CriterionInterface $criterion) use ($filter): bool { 366 | return ! $filter($criterion); 367 | }); 368 | } 369 | 370 | /** 371 | * @param string|\Closure $filter 372 | * @return Query 373 | */ 374 | public function only($filter): Query 375 | { 376 | $filter = $this->createFilter($filter); 377 | $copy = $this->clone(); 378 | $criteria = []; 379 | 380 | foreach ($copy->getCriteria() as $criterion) { 381 | if ($filter($criterion)) { 382 | $criteria[] = $criterion; 383 | } 384 | } 385 | 386 | $copy->criteria = $criteria; 387 | 388 | return $copy; 389 | } 390 | 391 | /** 392 | * @param string|callable $filter 393 | * @return callable 394 | */ 395 | private function createFilter($filter): callable 396 | { 397 | \assert(\is_string($filter) || \is_callable($filter)); 398 | 399 | if (\is_string($filter) && ! \is_callable($filter)) { 400 | $typeOf = $filter; 401 | 402 | return function (CriterionInterface $criterion) use ($typeOf): bool { 403 | return $criterion instanceof $typeOf; 404 | }; 405 | } 406 | 407 | return $filter; 408 | } 409 | 410 | /** 411 | * @return \Generator 412 | */ 413 | public function getIterator(): \Generator 414 | { 415 | foreach ($this->get() as $result) { 416 | yield $result; 417 | } 418 | } 419 | 420 | /** 421 | * @return bool 422 | */ 423 | public function isEmpty(): bool 424 | { 425 | return \count($this->criteria) === 0; 426 | } 427 | 428 | /** 429 | * @return string 430 | */ 431 | public function dump(): string 432 | { 433 | return $this->getRepository()->getProcessor()->dump($this); 434 | } 435 | 436 | /** 437 | * @return void 438 | */ 439 | private function bootIfNotBooted(): void 440 | { 441 | if (self::$booted === false) { 442 | self::$booted = true; 443 | 444 | $bootstrap = new Bootstrap(); 445 | $bootstrap->register($this->getRepository()->getEntityManager()); 446 | } 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /src/Query/AliasProvider.php: -------------------------------------------------------------------------------- 1 | alias === null) { 37 | $this->alias = $this->repository 38 | ? $this->createAlias($this->getRepository()->getClassName()) 39 | : $this->createAlias(); 40 | } 41 | 42 | return $this->alias; 43 | } 44 | 45 | /** 46 | * @param string ...$patterns 47 | * @return string 48 | */ 49 | public function createAlias(string ...$patterns): string 50 | { 51 | if (\count($patterns)) { 52 | $patterns = \array_map(function(string $pattern) { 53 | return \preg_replace('/\W+/iu', '', \snake_case(\class_basename($pattern))); 54 | }, $patterns); 55 | 56 | $pattern = \implode('_', $patterns); 57 | 58 | if (\trim($pattern)) { 59 | return \sprintf('%s_%d', $pattern, ++static::$lastQueryId); 60 | } 61 | } 62 | 63 | return 'q' . Str::random(7) . '_' . ++static::$lastQueryId; 64 | } 65 | 66 | /** 67 | * @param string|null $pattern 68 | * @return string 69 | */ 70 | public function createPlaceholder(string $pattern = null): string 71 | { 72 | return ':' . $this->createAlias($pattern); 73 | } 74 | 75 | /** 76 | * @param string $alias 77 | * @return Query|$this|self 78 | */ 79 | public function withAlias(string $alias): Query 80 | { 81 | $this->alias = $alias; 82 | 83 | foreach ($this->getCriteria() as $criterion) { 84 | $criterion->withAlias($alias); 85 | } 86 | 87 | return $this; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Query/ExecutionsProvider.php: -------------------------------------------------------------------------------- 1 | getRepository()->getProcessor(); 29 | 30 | return $processor->getResult($this, ...$fields); 31 | } 32 | 33 | /** 34 | * Get the values of a given key. 35 | * 36 | * @param string|array $value 37 | * @param string|null $key 38 | * @return Collection|iterable 39 | */ 40 | public function pluck($value, $key = null): array 41 | { 42 | return $this 43 | ->collect(...\array_filter([$value, $key])) 44 | ->pluck($value, $key) 45 | ->toArray(); 46 | } 47 | 48 | /** 49 | * @param string $field 50 | * @param string|null $typeOf 51 | * @return mixed 52 | * @throws \LogicException 53 | */ 54 | public function scalar(string $field, string $typeOf = null) 55 | { 56 | $processor = $this->getRepository()->getProcessor(); 57 | 58 | $result = $processor->getScalarResult($this, $field); 59 | 60 | if ($typeOf !== null) { 61 | return $this->cast($result, $typeOf); 62 | } 63 | 64 | return $result; 65 | } 66 | 67 | /** 68 | * @param mixed $result 69 | * @param string $typeOf 70 | * @return array|\Closure|object|mixed 71 | */ 72 | private function cast($result, string $typeOf) 73 | { 74 | $typeOf = \strtolower($typeOf); 75 | 76 | switch ($typeOf) { 77 | case 'callable': 78 | return function (callable $applicator = null) use ($result) { 79 | return ($applicator ?? '\\value')($result); 80 | }; 81 | 82 | case 'object': 83 | return (object)$result; 84 | 85 | case 'array': 86 | case 'iterable': 87 | return (array)$result; 88 | 89 | case 'string': 90 | return (string)$result; 91 | } 92 | 93 | $function = $typeOf . 'val'; 94 | 95 | if (! \function_exists($function)) { 96 | throw new \InvalidArgumentException('Could not cast to type ' . $typeOf); 97 | } 98 | 99 | return $function($result); 100 | } 101 | 102 | /** 103 | * @param string|null $field 104 | * @return int 105 | * @throws \LogicException 106 | */ 107 | public function count(string $field = null): int 108 | { 109 | if ($field === null) { 110 | $field = \array_first($this->getMetadata()->identifier); 111 | } 112 | 113 | return $this 114 | ->select('COUNT(' . $field . ') AS __count') 115 | ->scalar('__count', 'int'); 116 | } 117 | 118 | /** 119 | * @param string|null $field 120 | * @return int 121 | * @throws \LogicException 122 | */ 123 | public function sum(string $field = null): int 124 | { 125 | return $this 126 | ->select('SUM(' . $field . ') AS __sum') 127 | ->scalar('__sum', 'int'); 128 | } 129 | 130 | /** 131 | * @param string|null $field 132 | * @return int 133 | * @throws \LogicException 134 | */ 135 | public function avg(string $field = null): int 136 | { 137 | return $this 138 | ->select('AVG(' . $field . ') AS __avg') 139 | ->scalar('__avg', 'int'); 140 | } 141 | 142 | /** 143 | * @param string|null $field 144 | * @return int 145 | * @throws \LogicException 146 | */ 147 | public function max(string $field = null): int 148 | { 149 | return $this 150 | ->select('MAX(' . $field . ') AS __max') 151 | ->scalar('__max', 'int'); 152 | } 153 | 154 | /** 155 | * @param string|null $field 156 | * @return int 157 | * @throws \LogicException 158 | */ 159 | public function min(string $field = null): int 160 | { 161 | return $this 162 | ->select('MIN(' . $field . ') AS __min') 163 | ->scalar('__min', 'int'); 164 | } 165 | 166 | /** 167 | * @param string ...$fields 168 | * @return Collection 169 | */ 170 | public function collect(string ...$fields): Collection 171 | { 172 | return Collection::wrap($this->get(...$fields)); 173 | } 174 | 175 | /** 176 | * @param string[] $fields 177 | * @return object|null 178 | * @throws \LogicException 179 | */ 180 | public function first(string ...$fields) 181 | { 182 | return \array_first($this->get(...$fields)); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Query/GroupByProvider.php: -------------------------------------------------------------------------------- 1 | add(new GroupBy($this, $field)); 32 | } 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * @param string|\Closure $field 39 | * @param $valueOrOperator 40 | * @param null $value 41 | * @return Query|$this|self 42 | */ 43 | public function orHaving($field, $valueOrOperator = null, $value = null): self 44 | { 45 | return $this->or->having($field, $valueOrOperator, $value); 46 | } 47 | 48 | /** 49 | * @param string|\Closure $field 50 | * @param $valueOrOperator 51 | * @param null $value 52 | * @return Query|$this|self 53 | */ 54 | public function having($field, $valueOrOperator = null, $value = null): self 55 | { 56 | if (\is_string($field)) { 57 | [$operator, $value] = Having::completeMissingParameters($valueOrOperator, $value); 58 | 59 | return $this->add(new Having($this, $field, $operator, $value, $this->mode())); 60 | } 61 | 62 | if ($field instanceof \Closure) { 63 | return $this->add(new HavingGroup($this, $field, $this->mode())); 64 | } 65 | 66 | $error = \vsprintf('Selection set should be a type of string or Closure, but %s given', [ 67 | \studly_case(\gettype($field)), 68 | ]); 69 | 70 | throw new \InvalidArgumentException($error); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Query/LimitAndOffsetProvider.php: -------------------------------------------------------------------------------- 1 | limit($count); 31 | } 32 | 33 | /** 34 | * @param int $count 35 | * @return Query|$this|self 36 | */ 37 | public function limit(int $count): self 38 | { 39 | return $this->add(new Limit($this, $count)); 40 | } 41 | 42 | /** 43 | * An alias of "offset(...)" 44 | * 45 | * @param int $count 46 | * @return Query|$this|self 47 | */ 48 | public function skip(int $count): self 49 | { 50 | return $this->offset($count); 51 | } 52 | 53 | /** 54 | * @param int $count 55 | * @return Query|$this|self 56 | */ 57 | public function offset(int $count): self 58 | { 59 | return $this->add(new Offset($this, $count)); 60 | } 61 | 62 | /** 63 | * @param int $from 64 | * @param int $to 65 | * @return Query|$this|self 66 | */ 67 | public function range(int $from, int $to): self 68 | { 69 | if ($from > $to) { 70 | throw new \InvalidArgumentException('The "$from" value must be less than $to'); 71 | } 72 | 73 | return $this->limit($from)->offset($to - $from); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Query/ModeProvider.php: -------------------------------------------------------------------------------- 1 | conjunction = false; 42 | 43 | if ($group !== null) { 44 | $this->add($this->createGroup(__FUNCTION__, $group)); 45 | } 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @param \Closure|null $group 52 | * @return Query|$this|self 53 | */ 54 | public function and(\Closure $group = null): self 55 | { 56 | $this->conjunction = true; 57 | 58 | if ($group !== null) { 59 | $this->add($this->createGroup(__FUNCTION__, $group)); 60 | } 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * @param string $fn 67 | * @param \Closure $group 68 | * @return CriterionInterface 69 | */ 70 | private function createGroup(string $fn, \Closure $group): CriterionInterface 71 | { 72 | $latest = \count($this->criteria) 73 | ? \get_class(\array_last($this->criteria)) 74 | : null; 75 | 76 | switch($latest) { 77 | case Where::class: 78 | case WhereGroup::class: 79 | return new WhereGroup($this, $group, $this->mode()); 80 | 81 | case Having::class: 82 | case HavingGroup::class: 83 | return new HavingGroup($this, $group, $this->mode()); 84 | } 85 | 86 | $error = 'Operator "%s" can be added only after Where or Having clauses, but %s given'; 87 | $given = $latest ? \class_basename($latest) : 'none'; 88 | 89 | throw new \LogicException(\sprintf($error, \strtoupper($fn), $given)); 90 | } 91 | 92 | /** 93 | * @return bool 94 | */ 95 | protected function mode(): bool 96 | { 97 | return \tap($this->conjunction, function (): void { 98 | $this->conjunction = true; 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Query/OrderProvider.php: -------------------------------------------------------------------------------- 1 | where($field, $operator, $value)->asc($field); 33 | } 34 | 35 | /** 36 | * @param string ...$fields 37 | * @return Query|$this|self 38 | */ 39 | public function asc(string ...$fields): self 40 | { 41 | foreach ($fields as $field) { 42 | $this->orderBy($field); 43 | } 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * @param string $field 50 | * @param bool $asc 51 | * @return Query|$this|self 52 | */ 53 | public function orderBy(string $field, bool $asc = true): self 54 | { 55 | return $this->add(new OrderBy($this, $field, $asc)); 56 | } 57 | 58 | /** 59 | * @param string $field 60 | * @param mixed $value 61 | * @param bool $including 62 | * @return Query|$this|self 63 | */ 64 | public function before(string $field, $value, bool $including = false): self 65 | { 66 | $operator = $including ? Operator::LT : Operator::LTE; 67 | 68 | return $this->where($field, $operator, $value)->desc($field); 69 | } 70 | 71 | /** 72 | * @param string ...$fields 73 | * @return Query|$this|self 74 | */ 75 | public function desc(string ...$fields): self 76 | { 77 | foreach ($fields as $field) { 78 | $this->orderBy($field, false); 79 | } 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @param string $field 86 | * @return Query|$this|self 87 | */ 88 | public function latest(string $field = 'createdAt'): self 89 | { 90 | return $this->desc($field); 91 | } 92 | 93 | /** 94 | * @param string $field 95 | * @return Query|$this|self 96 | */ 97 | public function oldest(string $field = 'createdAt'): self 98 | { 99 | return $this->asc($field); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Query/RelationProvider.php: -------------------------------------------------------------------------------- 1 | leftJoin(...$relations); 30 | 31 | // return $this->addRelation(function(string $field, \Closure $inner = null) { 32 | // return new Relation($this, $field, $inner); 33 | // }, ...$relations); 34 | } 35 | 36 | /** 37 | * @param string|array ...$relations 38 | * @return Query|$this|self 39 | */ 40 | public function join(...$relations): self 41 | { 42 | return $this->addRelation(function(string $field, \Closure $inner = null) { 43 | return new Join($this, $field, Join::TYPE_JOIN, $inner); 44 | }, ...$relations); 45 | } 46 | 47 | /** 48 | * @param string|array ...$relations 49 | * @return Query|$this|self 50 | */ 51 | public function leftJoin(...$relations): self 52 | { 53 | return $this->addRelation(function(string $field, \Closure $inner = null) { 54 | return new Join($this, $field, Join::TYPE_LEFT_JOIN, $inner); 55 | }, ...$relations); 56 | } 57 | 58 | /** 59 | * @param string|array ...$relations 60 | * @return Query|$this|self 61 | */ 62 | public function innerJoin(...$relations): self 63 | { 64 | return $this->addRelation(function(string $field, \Closure $inner = null) { 65 | return new Join($this, $field, Join::TYPE_INNER_JOIN, $inner); 66 | }, ...$relations); 67 | } 68 | 69 | /** 70 | * @param \Closure $onCreate 71 | * @param string|array ...$relations 72 | * @return Query|$this|self 73 | */ 74 | private function addRelation(\Closure $onCreate, ...$relations): self 75 | { 76 | foreach ($relations as $relation) { 77 | if (\is_string($relation)) { 78 | $this->add($onCreate($relation)); 79 | continue; 80 | } 81 | 82 | if (\is_array($relation)) { 83 | foreach ($relation as $rel => $sub) { 84 | \assert(\is_string($rel) && $sub instanceof \Closure); 85 | 86 | $this->add($onCreate($rel, $sub)); 87 | } 88 | continue; 89 | } 90 | 91 | $error = 'Relation should be string ("relation_name") '. 92 | 'or array (["relation" => function]), ' . 93 | 'but %s given'; 94 | 95 | throw new \InvalidArgumentException(\sprintf($error, \gettype($relation))); 96 | } 97 | 98 | return $this; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Query/RepositoryProvider.php: -------------------------------------------------------------------------------- 1 | scope($this->repository = $repository); 34 | } 35 | 36 | /** 37 | * @return ObjectRepository|Hydrogen 38 | * @throws \LogicException 39 | */ 40 | public function getRepository(): ObjectRepository 41 | { 42 | if ($this->repository === null) { 43 | $error = 'Query should be attached to repository'; 44 | throw new \LogicException($error); 45 | } 46 | 47 | $this->bootIfNotBooted(); 48 | 49 | return $this->repository; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Query/SelectProvider.php: -------------------------------------------------------------------------------- 1 | "alias"]) or string ("field") but %s given'; 30 | throw new \InvalidArgumentException(\sprintf($error, \gettype($field))); 31 | } 32 | 33 | if (\is_string($field)) { 34 | $this->add(new Selection($this, $field)); 35 | continue; 36 | } 37 | 38 | foreach ($field as $name => $alias) { 39 | if (\is_int($name)) { 40 | [$name, $alias] = [$alias, null]; 41 | } 42 | 43 | $this->add(new Selection($this, $name, $alias)); 44 | } 45 | } 46 | return $this; 47 | } 48 | 49 | /** 50 | * @param string|null $alias 51 | * @return Query 52 | */ 53 | public function withEntity(string $alias = null): Query 54 | { 55 | return $this->select([':' . $this->getAlias() => $alias]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Query/WhereProvider.php: -------------------------------------------------------------------------------- 1 | or->where($field, $valueOrOperator, $value); 40 | } 41 | 42 | /** 43 | * @param string|\Closure $field 44 | * @param $valueOrOperator 45 | * @param null $value 46 | * @return Query|$this|self 47 | */ 48 | public function where($field, $valueOrOperator = null, $value = null): self 49 | { 50 | if (\is_string($field)) { 51 | [$operator, $value] = Where::completeMissingParameters($valueOrOperator, $value); 52 | 53 | return $this->add(new Where($this, $field, $operator, $value, $this->mode())); 54 | } 55 | 56 | if ($field instanceof \Closure) { 57 | return $this->add(new WhereGroup($this, $field, $this->mode())); 58 | } 59 | 60 | $error = \vsprintf('Selection set should be a type of string or Closure, but %s given', [ 61 | \studly_case(\gettype($field)), 62 | ]); 63 | 64 | throw new \InvalidArgumentException($error); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Query/WhereProvider/WhereBetweenProvider.php: -------------------------------------------------------------------------------- 1 | or()->whereBetween($field, $from, $to); 30 | } 31 | 32 | /** 33 | * @param string $field 34 | * @param mixed $from 35 | * @param mixed $to 36 | * @return Query|$this|self 37 | */ 38 | public function whereBetween(string $field, $from, $to): self 39 | { 40 | return $this->where($field, Operator::BTW, [$from, $to]); 41 | } 42 | 43 | /** 44 | * @param string $field 45 | * @param mixed $from 46 | * @param mixed $to 47 | * @return Query|$this|self 48 | */ 49 | public function orWhereNotBetween(string $field, $from, $to): self 50 | { 51 | return $this->or()->whereNotBetween($field, $from, $to); 52 | } 53 | 54 | /** 55 | * @param string $field 56 | * @param mixed $from 57 | * @param mixed $to 58 | * @return Query|$this|self 59 | */ 60 | public function whereNotBetween(string $field, $from, $to): self 61 | { 62 | return $this->where($field, Operator::NOT_BTW, [$from, $to]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Query/WhereProvider/WhereInProvider.php: -------------------------------------------------------------------------------- 1 | or()->whereIn($field, $value); 30 | } 31 | 32 | /** 33 | * @param string $field 34 | * @param iterable|array $value 35 | * @return Query|$this|self 36 | */ 37 | public function whereIn(string $field, iterable $value): self 38 | { 39 | return $this->where($field, Operator::IN, $value); 40 | } 41 | 42 | /** 43 | * @param string $field 44 | * @param iterable $value 45 | * @return Query|$this|self 46 | */ 47 | public function orWhereNotIn(string $field, iterable $value): self 48 | { 49 | return $this->or()->whereNotIn($field, $value); 50 | } 51 | 52 | /** 53 | * @param string $field 54 | * @param iterable $value 55 | * @return Query|$this|self 56 | */ 57 | public function whereNotIn(string $field, iterable $value): self 58 | { 59 | return $this->where($field, Operator::NOT_IN, $value); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Query/WhereProvider/WhereLikeProvider.php: -------------------------------------------------------------------------------- 1 | where($field, Operator::LIKE, $value); 29 | } 30 | 31 | /** 32 | * @param string $field 33 | * @param string|mixed $value 34 | * @return Query|$this 35 | */ 36 | public function notLike(string $field, $value): self 37 | { 38 | return $this->where($field, Operator::NOT_LIKE, $value); 39 | } 40 | 41 | /** 42 | * @param string $field 43 | * @param string|mixed $value 44 | * @return Query|$this 45 | */ 46 | public function orLike(string $field, $value): self 47 | { 48 | return $this->or()->where($field, Operator::LIKE, $value); 49 | } 50 | 51 | /** 52 | * @param string $field 53 | * @param string|mixed $value 54 | * @return Query|$this 55 | */ 56 | public function orNotLike(string $field, $value): self 57 | { 58 | return $this->or()->where($field, Operator::NOT_LIKE, $value); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Query/WhereProvider/WhereNullProvider.php: -------------------------------------------------------------------------------- 1 | or()->whereNull($field); 29 | } 30 | 31 | /** 32 | * @param string $field 33 | * @return Query|$this|self 34 | */ 35 | public function whereNull(string $field): self 36 | { 37 | return $this->add(new Where($this, $field, Operator::EQ, null, $this->mode())); 38 | } 39 | 40 | /** 41 | * @param string $field 42 | * @return Query|$this|self 43 | */ 44 | public function orWhereNotNull(string $field): self 45 | { 46 | return $this->or()->whereNotNull($field); 47 | } 48 | 49 | /** 50 | * @param string $field 51 | * @return Query|$this|self 52 | */ 53 | public function whereNotNull(string $field): self 54 | { 55 | return $this->add(new Where($this, $field, Operator::NEQ, null, $this->mode())); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | 18 | * $array = Collection::make(...)->map->intval(_, 10)->toArray(); 19 | * 20 | * // Is similar with: 21 | * 22 | * $array = \array_map(function ($item): int { 23 | * return \intval($item, 10); 24 | * ^^^^^ - pattern "_" will replaced to each delegated item value. 25 | * }, ...); 26 | * 27 | */ 28 | if (! \defined('_')) { 29 | \define('_', \RDS\Hydrogen\HighOrderMessaging\HigherOrderCollectionProxy::PATTERN); 30 | } 31 | 32 | // --------------------------------------------- 33 | // Polyfills 34 | // --------------------------------------------- 35 | 36 | /** 37 | * @since 0.3.4 38 | */ 39 | if (! \class_exists(\RDS\Hydrogen\Collection\Collection::class)) { 40 | \class_alias(\RDS\Hydrogen\Collection::class, \RDS\Hydrogen\Collection\Collection::class); 41 | } 42 | } 43 | --------------------------------------------------------------------------------