Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 13 commits into from
Mar 4, 2024
Merged

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

merged 13 commits into from
Mar 4, 2024

Conversation

musiermoore
Copy link
Contributor

@musiermoore musiermoore commented Mar 2, 2024

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%"
)

Copy link

github-actions bot commented Mar 2, 2024

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
Copy link
Contributor

deleugpn commented Mar 3, 2024

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

@fenicfelix
Copy link

I love how this simplified the nested query.

@musiermoore
Copy link
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
Copy link
Contributor

johanrosenson commented Mar 3, 2024

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
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
Copy link
Contributor Author

musiermoore commented Mar 3, 2024

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
Copy link
Contributor

johanrosenson commented Mar 4, 2024

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
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
24 checks passed
@MrPunyapal
Copy link
Contributor

Time to remove whereLike macro 👀

@andrey-helldar
Copy link
Contributor

andrey-helldar commented Mar 6, 2024

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
Copy link

kolydart commented Mar 7, 2024

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
Copy link

richt222 commented Mar 7, 2024

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
Copy link

kolydart commented Mar 7, 2024

You are right. I edited the snipped.

Copy link

@Williamug Williamug left a comment

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