Skip to content

[10.x] Add whereAll and whereAny methods to the query builder - #50344

Merged
taylorotwell merged 13 commits into
laravel:10.xfrom
musiermoore:feature/where-multiple
Mar 4, 2024
Merged

[10.x] Add whereAll and whereAny methods to the query builder#50344
taylorotwell merged 13 commits into
laravel:10.xfrom
musiermoore:feature/where-multiple

Conversation

@musiermoore

@musiermoore musiermoore commented Mar 2, 2024

Copy link
Copy Markdown
Contributor

Hi,

Sometimes I need to make a search input on some pages where one value is compared with multiple columns. For example, an admin needs to able to search users by first name, last name, email or phone. Usually I wrap orWhere in another where as a closure:

$search = '%Otwell%';

User::query()
      ->where(function ($query) use ($search) {
          $query
              ->where('first_name', 'LIKE', $search)
              ->orWhere('last_name', 'LIKE', $search)
              ->orWhere('email', 'LIKE', $search)
              ->orWhere('phone', 'LIKE', $search);
      })
      ...

We can create a scope 'search' or something, but it's anyway a lot of where in where, so I always wanted something like that:

$search = '%Otwell%';

User::query()
      ->whereMultiple([
          'first_name',
          'last_name',
          'email',
          'phone'
      ], 'LIKE', $search)
      ...

The method has these params:

whereMultiple($columns, $operator = null, $value = null, $columnsBoolean = 'or', $boolean = 'and')

$columnsBoolean is the boolean type between passed columns:

// $columnsBoolean = 'or'
where (column_1 = 'text' or column_2 = 'text' or column_3 = 'text')

// $columnsBoolean = 'and'
where (column_1 = 'text' and column_2 = 'text' and column_3 = 'text')

The method creates a closure and where for each passed column with or or and. It looks like:

select * from "users" where (
  "first_name" like "%Otwell%" 
  or "last_name" like "%Otwell%" 
  or "email" like "%Otwell%"
  or "phone" like "%Otwell%"
)

I haven't found something similar, except putting an array in where, but this is not the same. I hope I didn't miss the same functionality. If I did, please let me know

It's my first PR, so if something is wrong or you just have any questions, let me know

Thanks,
Alex

Updates

whereMultiple has been split into whereAll and whereAny.

whereAll

Creates a nested query that uses the same operator and value for each column passed in, where ALL columns must match the value and operator.

Params:

whereAll($columns, $operator = null, $value = null, $boolean = 'and')

Usage:

$search = 'test';

User::whereAll([
  'first_name',
  'last_name',
  'email',
], 'LIKE', "%$search%")
...

All columns has the same operator LIKE and the same value "%test%", and they have AND between each column:

SELECT * FROM "users" WHERE (
  "first_name" LIKE "%test%" 
  AND "last_name" LIKE "%test%" 
  AND "email" LIKE "%test%"
)

whereAny

Creates a nested query that uses the same operator and value for each column passed in, where ANY of the columns must match the value and operator.

Params:

whereAll($columns, $operator = null, $value = null, $boolean = 'and')

Usage:

$search = 'test';

User::whereAny([
  'first_name',
  'last_name',
  'email',
], 'LIKE', "%$search%")
...

All columns has the same operator LIKE and the same value "%test%", and they have OR between each column:

SELECT * FROM "users" WHERE (
  "first_name" LIKE "%test%" 
  OR "last_name" LIKE "%test%" 
  OR "email" LIKE "%test%"
)

@github-actions

github-actions Bot commented Mar 2, 2024

Copy link
Copy Markdown

Thanks for submitting a PR!

Note that draft PR's are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

@GrahamCampbell GrahamCampbell changed the title Add whereMultiple method to the query builder [10.x] Add whereMultiple method to the query builder Mar 2, 2024
@deleugpn

deleugpn commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

Damn, I didn't know I needed this until today

@fenicfelix

Copy link
Copy Markdown

I love how this simplified the nested query.

@musiermoore

Copy link
Copy Markdown
Contributor Author

Yesterday this test tests/Database/QueryDurationThresholdTest/testItIsOnlyCalledOnceWhenGivenDateTime had been failed on PHP 8.1 - stable - Windows, and today the test is successful after 2 builds, but I haven't made any changes except formatting. Something went wrong with the tests or my code might affect anything? 🤔

Just want to make sure everything is ok 😅

@musiermoore
musiermoore marked this pull request as ready for review March 3, 2024 04:09
@johanrosenson

johanrosenson commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

For me it wasn't obvious by the name that the default value of $columnsBoolean is or, I would have guessed that it was and just by the name of the method.

How about instead of only one method, split it into four methods? That would allow you to simplify the signature greatly and also make the naming more obvious.

My suggestion:

whereAll($columns, $operator = null, $value = null); // same as: $columnsBoolean = 'and', $boolean = 'and'
orWhereAll($columns, $operator = null, $value = null); // same as: $columnsBoolean = 'and', $boolean = 'or'
whereAny($columns, $operator = null, $value = null); // same as: $columnsBoolean = 'or', $boolean = 'and'
orWhereAny($columns, $operator = null, $value = null); // same as: $columnsBoolean = 'or', $boolean = 'or'

IMO this would also match the pattern of the existing methods where / orWhere.

@musiermoore

Copy link
Copy Markdown
Contributor Author

@johanrosenson oh, yes, you're right, it looks better and more intuitive 🤝 thanks for the idea. I'm working on it soon 🙃

@musiermoore musiermoore changed the title [10.x] Add whereMultiple method to the query builder [10.x] Add whereAll and whereAny methods to the query builder Mar 3, 2024
@musiermoore

musiermoore commented Mar 3, 2024

Copy link
Copy Markdown
Contributor Author

I've split whereMultiple to whereAll and whereAny (and or versions) and added tests for the four methods

Also changed the visibility of the old method whereMultiple to private to avoid duplication, does it make sense? But I haven't found at least one private method in the query builder, is there a place where I should put the private method?

If I delete the old method, each new method will have something like that:

[$value, $operator] = $this->prepareValueAndOperator(
    $value, $operator, empty($value) && $this->invalidOperator($operator)
);

$this->whereNested(function ($query) use ($columns, $operator, $value, $columnsBoolean) {
    foreach ($columns as $column) {
        $query->where($column, $operator, $value, 'and'); // whereAll
        // or
        $query->where($column, $operator, $value, 'or'); // whereAny
    }
}, $boolean);

Any ideas? 🤔

@johanrosenson

johanrosenson commented Mar 4, 2024

Copy link
Copy Markdown
Contributor

If I delete the old method, each new method will have something like that:

[$value, $operator] = $this->prepareValueAndOperator(
    $value, $operator, empty($value) && $this->invalidOperator($operator)
);

$this->whereNested(function ($query) use ($columns, $operator, $value, $columnsBoolean) {
    foreach ($columns as $column) {
        $query->where($column, $operator, $value, 'and'); // whereAll
        // or
        $query->where($column, $operator, $value, 'or'); // whereAny
    }
}, $boolean);

If this was my code I would have been ok with some duplication to make it easier to understand each method, I would have used your code above in whereAll and whereAny and I would have left your orWhereAll and orWhereAny unchanged like you have them now.

Then there would be no need for the private whereMultiple / orWhereMultiple

@musiermoore

Copy link
Copy Markdown
Contributor Author

Thanks for your help! 🫡

Now there are only 4 methods (whereAll, orWhereAll, whereAny and orWhereAny) 🎉

@taylorotwell
taylorotwell merged commit 5050195 into laravel:10.x Mar 4, 2024
@MrPunyapal

Copy link
Copy Markdown
Contributor

Time to remove whereLike macro 👀

@andrey-helldar

andrey-helldar commented Mar 6, 2024

Copy link
Copy Markdown
Contributor

In my case, you need to fix the keyboard layout in case the user didn't.
Also, taking into account that search can be done by partial match in identifier, the macro looked like this:

Before Laravel 10.47
class BuilderServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->bootWhereLike();
        $this->bootOrWhereLike();
        $this->bootOrWhereLikeColumns();
    }

    protected function bootWhereLike(): void
    {
        Builder::macro('whereLike', function (string $column, int | string $value, string $operator = 'and') {
            $column = DB::raw("lower($column::text)");

            $cyrillic = is_numeric($value) ? $value : Keyboard::toCyrillic($value);
            $latin = is_numeric($value) ? $value : Keyboard::toLatin($value);

            return $this->where(
                column: fn(Builder $query) => $query
                    ->when(
                        $cyrillic === $latin,
                        fn(Builder $query) => $query
                            ->where($column, 'like', "%$cyrillic%")
                            ->when(
                                $cyrillic !== $value,
                                fn(Builder $query) => $query
                                    ->orWhere($column, 'like', "%$value%")
                            ),
                        fn(Builder $query) => $query
                            ->where($column, 'like', "%$cyrillic%")
                            ->orWhere($column, 'like', "%$latin%")
                            ->when(
                                !in_array($value, [$cyrillic, $latin], true),
                                fn(Builder $query) => $query
                                    ->orWhere($column, 'like', "%$value%")
                            ),
                    ),
                boolean: $operator
            );
        });
    }

    protected function bootOrWhereLike(): void
    {
        Builder::macro('orWhereLike', function (string $column, int | string $value) {
            return $this->whereLike($column, $value, 'or');
        });
    }

    protected function bootOrWhereLikeColumns(): void
    {
        Builder::macro('orWhereLikeColumns', function (array $columns, int | string $value) {
            return $this->where(function (Builder $query) use ($columns, $value) {
                foreach ($columns as $column) {
                    $query->orWhereLike($column, $value);
                }

                return $query;
            });
        });
    }
}
Laravel 10.47+
class BuilderServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->whereLikeColumns();
    }

    protected function whereLikeColumns(): void
    {
        Builder::macro('whereLikeColumns', function (array | string $columns, int | string $value) {
            $columns = collect(Arr::wrap($columns))->map(
                fn(string $column) => DB::raw("lower($column::text)")
            )->all();

            $cyrillic = is_numeric($value) ? $value : Keyboard::toCyrillic($value);
            $latin = is_numeric($value) ? $value : Keyboard::toLatin($value);

            return $this->where(function (Builder $query) use ($columns, $value, $cyrillic, $latin) {
                foreach (array_unique([$value, $cyrillic, $latin]) as $search) {
                    $query->orWhereAny($columns, 'LIKE', "%$search%");
                }
            });
        });
    }
}

That said, they are used in almost the same way:

// < 10.47
SomeModel::query()
    ->when(
        $data->query,
        fn (Builder $builder, int | string $query) => $builder->orWhereLikeColumns(['id', 'title'], $query)
    )

// 10.47+
SomeModel::query()
    ->when(
        $data->query,
        fn (Builder $builder, int | string $query) => $builder->whereLikeColumns(['id', 'title'], $query)
    )

@kolydart

kolydart commented Mar 7, 2024

Copy link
Copy Markdown

I don't get it for whereAll.
Isn't where capable of muliple criteria?

Is there a difference between this snippet using whereAll

User::whereAll([
  'first_name',
  'last_name',
  'email',
], 'LIKE', "%$search%")

and this snippet using plain where?

User::where([
  ['first_name', 'LIKE', "%$search%"],
  ['last_name', 'LIKE', "%$search%"],
  ['email', 'LIKE', "%$search%"],
])

I mean, why should I learn a new command, when I can do the same thing with an existing command?

@richt222

richt222 commented Mar 7, 2024

Copy link
Copy Markdown

I mean, why should I learn a new command, when I can do the same thing with an existing command?

whereAll feels far more fluid and readable IMO.

In your second snippet I think you would also need to wrap those comparison arrays in a parent array? With this PR, it isn't necessary as the columns are already grouped.

Nice addition IMO. I can see it being used a lot.

@kolydart

kolydart commented Mar 7, 2024

Copy link
Copy Markdown

You are right. I edited the snipped.

@Williamug Williamug left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So cool 🔥 🔥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants