├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 | 
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 |
--------------------------------------------------------------------------------