Skip to content
This repository was archived by the owner on Aug 22, 2023. It is now read-only.

Commit 9d54e0f

Browse files
committed
PHPORM-53 Fix and test like and regex operators
- Fix `like` and `not like` operators to be case-sensitive. - Add `ilike` and `not ilike` operators, equivalent to `like` and `not like` but case-insensitive. - Fix and optimize `regex` and `not regex` operators, and their aliases `regexp` and `not regexp`.
1 parent 9d9c7c8 commit 9d54e0f

File tree

3 files changed

+95
-16
lines changed

3 files changed

+95
-16
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ All notable changes to this project will be documented in this file.
33

44
## [Unreleased]
55

6-
- Add classes to cast ObjectId and UUID instances [#1](https://github.com/GromNaN/laravel-mongodb-private/pull/1) by [@alcaeus](https://github.com/alcaeus).
6+
- Add classes to cast `ObjectId` and `UUID` instances [#1](https://github.com/GromNaN/laravel-mongodb-private/pull/1) by [@alcaeus](https://github.com/alcaeus).
77
- Add `Query\Builder::toMql()` to simplify comprehensive query tests [#6](https://github.com/GromNaN/laravel-mongodb-private/pull/6) by [@GromNaN](https://github.com/GromNaN).
88
- Fix `Query\Builder::whereNot` to use MongoDB [`$not`](https://www.mongodb.com/docs/manual/reference/operator/query/not/) operator [#13](https://github.com/GromNaN/laravel-mongodb-private/pull/13) by [@GromNaN](https://github.com/GromNaN).
99
- Fix `Query\Builder::whereBetween` to accept `Carbon\Period` object [#10](https://github.com/GromNaN/laravel-mongodb-private/pull/10) by [@GromNaN](https://github.com/GromNaN).
1010
- Throw an exception for unsupported `Query\Builder` methods [#9](https://github.com/GromNaN/laravel-mongodb-private/pull/9) by [@GromNaN](https://github.com/GromNaN).
1111
- Throw an exception when `Query\Builder::orderBy()` is used with invalid direction [#7](https://github.com/GromNaN/laravel-mongodb-private/pull/7) by [@GromNaN](https://github.com/GromNaN).
1212
- Throw an exception when `Query\Builder::push()` is used incorrectly [#5](https://github.com/GromNaN/laravel-mongodb-private/pull/5) by [@GromNaN](https://github.com/GromNaN).
13+
- Update `like` operators to be case-sensitive and improve support for `ilike`, `regex`, `regexp` and their `not` variants [#17](https://github.com/GromNaN/laravel-mongodb-private/pull/17) by [@GromNaN](https://github.com/GromNaN).
1314

1415
## [3.9.2] - 2022-09-01
1516

src/Query/Builder.php

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class Builder extends BaseBuilder
8383
'not like',
8484
'between',
8585
'ilike',
86+
'not ilike',
8687
'&',
8788
'|',
8889
'^',
@@ -98,6 +99,7 @@ class Builder extends BaseBuilder
9899
'all',
99100
'size',
100101
'regex',
102+
'not regex',
101103
'text',
102104
'slice',
103105
'elemmatch',
@@ -964,6 +966,7 @@ protected function compileWheres(): array
964966
// Operator conversions
965967
$convert = [
966968
'regexp' => 'regex',
969+
'not regexp' => 'not regex',
967970
'elemmatch' => 'elemMatch',
968971
'geointersects' => 'geoIntersects',
969972
'geowithin' => 'geoWithin',
@@ -1074,9 +1077,10 @@ protected function compileWhereBasic(array $where): array
10741077
{
10751078
extract($where);
10761079

1077-
// Replace like or not like with a Regex instance.
1078-
if (in_array($operator, ['like', 'not like'])) {
1079-
if ($operator === 'not like') {
1080+
// Replace like with a Regex instance.
1081+
if (in_array($operator, ['like', 'not like', 'ilike', 'not ilike'])) {
1082+
$flags = str_ends_with($operator, 'ilike') ? 'i' : '';
1083+
if (str_starts_with($operator, 'not')) {
10801084
$operator = 'not';
10811085
} else {
10821086
$operator = '=';
@@ -1086,28 +1090,33 @@ protected function compileWhereBasic(array $where): array
10861090
$regex = preg_replace('#(^|[^\\\])%#', '$1.*', preg_quote($value));
10871091

10881092
// Convert like to regular expression.
1089-
if (! Str::startsWith($value, '%')) {
1093+
if (! str_starts_with($value, '%')) {
10901094
$regex = '^'.$regex;
10911095
}
1092-
if (! Str::endsWith($value, '%')) {
1096+
if (! str_ends_with($value, '%')) {
10931097
$regex .= '$';
10941098
}
10951099

1096-
$value = new Regex($regex, 'i');
1097-
} // Manipulate regexp operations.
1098-
elseif (in_array($operator, ['regexp', 'not regexp', 'regex', 'not regex'])) {
1100+
$value = new Regex($regex, $flags);
1101+
} // Manipulate regex operations.
1102+
elseif (in_array($operator, ['regex', 'not regex'])) {
10991103
// Automatically convert regular expression strings to Regex objects.
11001104
if (! $value instanceof Regex) {
11011105
$e = explode('/', $value);
1102-
$flag = end($e);
1103-
$regstr = substr($value, 1, -(strlen($flag) + 1));
1104-
$value = new Regex($regstr, $flag);
1106+
if ($value[0] !== '/' || count($e) < 3) {
1107+
throw new \InvalidArgumentException(sprintf('Regular expressions must be surrounded by slashes "/". Got "%s"', $value));
1108+
}
1109+
$flags = end($e);
1110+
$regstr = substr($value, 1, -1 - strlen($flags));
1111+
$value = new Regex($regstr, $flags);
11051112
}
11061113

1107-
// For inverse regexp operations, we can just use the $not operator
1108-
// and pass it a Regex instence.
1109-
if (Str::startsWith($operator, 'not')) {
1114+
// For inverse regex operations, we can just use the $not operator
1115+
// and pass it a Regex instance.
1116+
if (str_starts_with($operator, 'not')) {
11101117
$operator = 'not';
1118+
} else {
1119+
$operator = '=';
11111120
}
11121121
}
11131122

tests/Query/BuilderTest.php

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Jenssegers\Mongodb\Query\Builder;
1212
use Jenssegers\Mongodb\Query\Processor;
1313
use Mockery as m;
14+
use MongoDB\BSON\Regex;
1415
use MongoDB\BSON\UTCDateTime;
1516
use PHPUnit\Framework\TestCase;
1617

@@ -433,6 +434,62 @@ function (Builder $builder) {
433434
->orWhereNotBetween('id', collect([3, 4])),
434435
];
435436

437+
yield 'where like' => [
438+
['find' => [['name' => new Regex('^acme$', '')], []]],
439+
fn (Builder $builder) => $builder->where('name', 'like', 'acme'),
440+
];
441+
442+
yield 'where like escape' => [
443+
['find' => [['name' => new Regex('^\^acme\$$', '')], []]],
444+
fn (Builder $builder) => $builder->where('name', 'like', '^acme$'),
445+
];
446+
447+
yield 'where like %' => [
448+
['find' => [['name' => new Regex('.*acme.*', '')], []]],
449+
fn (Builder $builder) => $builder->where('name', 'like', '%acme%'),
450+
];
451+
452+
yield 'where ilike' => [
453+
['find' => [['name' => new Regex('^acme$', 'i')], []]],
454+
fn (Builder $builder) => $builder->where('name', 'ilike', 'acme'),
455+
];
456+
457+
yield 'where not like' => [
458+
['find' => [['name' => ['$not' => new Regex('^acme$', '')]], []]],
459+
fn (Builder $builder) => $builder->where('name', 'not like', 'acme'),
460+
];
461+
462+
yield 'where not ilike' => [
463+
['find' => [['name' => ['$not' => new Regex('^acme$', 'i')]], []]],
464+
fn (Builder $builder) => $builder->where('name', 'not ilike', 'acme'),
465+
];
466+
467+
$regex = new Regex('^acme$', 'si');
468+
yield 'where BSON\Regex' => [
469+
['find' => [['name' => $regex], []]],
470+
fn (Builder $builder) => $builder->where('name', 'regex', $regex),
471+
];
472+
473+
yield 'where regex' => [
474+
['find' => [['name' => $regex], []]],
475+
fn (Builder $builder) => $builder->where('name', 'regex', '/^acme$/si'),
476+
];
477+
478+
yield 'where not regex' => [
479+
['find' => [['name' => ['$not' => $regex]], []]],
480+
fn (Builder $builder) => $builder->where('name', 'not regex', '/^acme$/si'),
481+
];
482+
483+
yield 'where regexp' => [
484+
['find' => [['name' => $regex], []]],
485+
fn (Builder $builder) => $builder->where('name', 'regexp', '/^acme$/si'),
486+
];
487+
488+
yield 'where not regexp' => [
489+
['find' => [['name' => ['$not' => $regex]], []]],
490+
fn (Builder $builder) => $builder->where('name', 'not regexp', '/^acme$/si'),
491+
];
492+
436493
yield 'distinct' => [
437494
['distinct' => ['foo', [], []]],
438495
fn (Builder $builder) => $builder->distinct('foo'),
@@ -456,7 +513,7 @@ public function testException($class, $message, \Closure $build): void
456513

457514
$this->expectException($class);
458515
$this->expectExceptionMessage($message);
459-
$build($builder);
516+
$build($builder)->toMQL();
460517
}
461518

462519
public static function provideExceptions(): iterable
@@ -497,6 +554,18 @@ public static function provideExceptions(): iterable
497554
'Between $values must be a list with exactly two elements: [min, max]',
498555
fn (Builder $builder) => $builder->whereBetween('id', ['min' => 1, 'max' => 2]),
499556
];
557+
558+
yield 'where regex not starting with /' => [
559+
\InvalidArgumentException::class,
560+
'Regular expressions must be surrounded by slashes "/". Got "^ac/me$"',
561+
fn (Builder $builder) => $builder->where('name', 'regex', '^ac/me$'),
562+
];
563+
564+
yield 'where regex not ending with /' => [
565+
\InvalidArgumentException::class,
566+
'Regular expressions must be surrounded by slashes "/". Got "/^acme$"',
567+
fn (Builder $builder) => $builder->where('name', 'regex', '/^acme$'),
568+
];
500569
}
501570

502571
/** @dataProvider getEloquentMethodsNotSupported */

0 commit comments

Comments
 (0)