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

Commit 074b26f

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 52c0ea3 commit 074b26f

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,14 +3,15 @@ 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).
1313
- Remove public property `Query\Builder::$paginating` [#15](https://github.com/GromNaN/laravel-mongodb-private/pull/15) by [@GromNaN](https://github.com/GromNaN).
14+
- 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).
1415

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

src/Query/Builder.php

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class Builder extends BaseBuilder
7676
'not like',
7777
'between',
7878
'ilike',
79+
'not ilike',
7980
'&',
8081
'|',
8182
'^',
@@ -91,6 +92,7 @@ class Builder extends BaseBuilder
9192
'all',
9293
'size',
9394
'regex',
95+
'not regex',
9496
'text',
9597
'slice',
9698
'elemmatch',
@@ -947,6 +949,7 @@ protected function compileWheres(): array
947949
// Operator conversions
948950
$convert = [
949951
'regexp' => 'regex',
952+
'not regexp' => 'not regex',
950953
'elemmatch' => 'elemMatch',
951954
'geointersects' => 'geoIntersects',
952955
'geowithin' => 'geoWithin',
@@ -1057,9 +1060,10 @@ protected function compileWhereBasic(array $where): array
10571060
{
10581061
extract($where);
10591062

1060-
// Replace like or not like with a Regex instance.
1061-
if (in_array($operator, ['like', 'not like'])) {
1062-
if ($operator === 'not like') {
1063+
// Replace like with a Regex instance.
1064+
if (in_array($operator, ['like', 'not like', 'ilike', 'not ilike'])) {
1065+
$flags = str_ends_with($operator, 'ilike') ? 'i' : '';
1066+
if (str_starts_with($operator, 'not')) {
10631067
$operator = 'not';
10641068
} else {
10651069
$operator = '=';
@@ -1069,28 +1073,33 @@ protected function compileWhereBasic(array $where): array
10691073
$regex = preg_replace('#(^|[^\\\])%#', '$1.*', preg_quote($value));
10701074

10711075
// Convert like to regular expression.
1072-
if (! Str::startsWith($value, '%')) {
1076+
if (! str_starts_with($value, '%')) {
10731077
$regex = '^'.$regex;
10741078
}
1075-
if (! Str::endsWith($value, '%')) {
1079+
if (! str_ends_with($value, '%')) {
10761080
$regex .= '$';
10771081
}
10781082

1079-
$value = new Regex($regex, 'i');
1080-
} // Manipulate regexp operations.
1081-
elseif (in_array($operator, ['regexp', 'not regexp', 'regex', 'not regex'])) {
1083+
$value = new Regex($regex, $flags);
1084+
} // Manipulate regex operations.
1085+
elseif (in_array($operator, ['regex', 'not regex'])) {
10821086
// Automatically convert regular expression strings to Regex objects.
10831087
if (! $value instanceof Regex) {
10841088
$e = explode('/', $value);
1085-
$flag = end($e);
1086-
$regstr = substr($value, 1, -(strlen($flag) + 1));
1087-
$value = new Regex($regstr, $flag);
1089+
if ($value[0] !== '/' || count($e) < 3) {
1090+
throw new \InvalidArgumentException(sprintf('Regular expressions must be surrounded by slashes "/". Got "%s"', $value));
1091+
}
1092+
$flags = end($e);
1093+
$regstr = substr($value, 1, -1 - strlen($flags));
1094+
$value = new Regex($regstr, $flags);
10881095
}
10891096

1090-
// For inverse regexp operations, we can just use the $not operator
1091-
// and pass it a Regex instence.
1092-
if (Str::startsWith($operator, 'not')) {
1097+
// For inverse regex operations, we can just use the $not operator
1098+
// and pass it a Regex instance.
1099+
if (str_starts_with($operator, 'not')) {
10931100
$operator = 'not';
1101+
} else {
1102+
$operator = '=';
10941103
}
10951104
}
10961105

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

@@ -439,6 +440,62 @@ function (Builder $builder) {
439440
->orWhereNotBetween('id', collect([3, 4])),
440441
];
441442

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

463520
$this->expectException($class);
464521
$this->expectExceptionMessage($message);
465-
$build($builder);
522+
$build($builder)->toMQL();
466523
}
467524

468525
public static function provideExceptions(): iterable
@@ -503,6 +560,18 @@ public static function provideExceptions(): iterable
503560
'Between $values must be a list with exactly two elements: [min, max]',
504561
fn (Builder $builder) => $builder->whereBetween('id', ['min' => 1, 'max' => 2]),
505562
];
563+
564+
yield 'where regex not starting with /' => [
565+
\InvalidArgumentException::class,
566+
'Regular expressions must be surrounded by slashes "/". Got "^ac/me$"',
567+
fn (Builder $builder) => $builder->where('name', 'regex', '^ac/me$'),
568+
];
569+
570+
yield 'where regex not ending with /' => [
571+
\InvalidArgumentException::class,
572+
'Regular expressions must be surrounded by slashes "/". Got "/^acme$"',
573+
fn (Builder $builder) => $builder->where('name', 'regex', '/^acme$'),
574+
];
506575
}
507576

508577
/** @dataProvider getEloquentMethodsNotSupported */

0 commit comments

Comments
 (0)