Skip to content

[12.x] Add whereHasAny, orWhereHasAny, whereHasAll, and orWhereHasAll Methods to Eloquent Builder #55563

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

Draft
wants to merge 4 commits into
base: 12.x
Choose a base branch
from

Conversation

ezequidias
Copy link

@ezequidias ezequidias commented Apr 26, 2025

Even though PR #55303 was rejected, I truly believe that Eloquent should offer native support for whereAny across relationships.

Working with complex nested relationships is a daily reality for many developers building real-world applications.
Currently, chaining multiple whereHas and orWhereHas conditions manually makes the code repetitive, harder to read, and more prone to mistakes.

Adding first-class support for whereHasAny, orWhereHasAny, whereHasAll, and orWhereHasAll would not only simplify our queries — it would empower developers to express their intentions more clearly, with better performance and less boilerplate.

I genuinely believe that Eloquent deserves this improvement.
It would make a real difference for many of us working with complex data structures every day.

This PR introduces four new methods to the Eloquent Builder:

  • Add whereHasAny method
  • Add orWhereHasAny method
  • Add whereHasAll method
  • Add orWhereHasAll method
  • Add corresponding tests

These new methods allow for more expressive and concise querying when filtering models based on multiple nested relationships combined by "any" (OR) or "all" (AND) logic across specific columns.

Motivation

Previously, to perform a search across multiple nested relationships, developers had to manually chain multiple whereHas and orWhereHas statements, leading to repetitive, verbose, and harder-to-maintain code.

This feature improves readability, developer experience, and maintainability, while keeping the flexibility and performance of Eloquent queries.

Before:

User::query()
   ->whereHas('orders.orderItems.product.category', function (Builder $query) use ($search) {
        $query->where('name', $search);
    })
    ->orWhereHas('orders.orderItems.product', function (Builder $query) use ($search) {
        $query->whereAny(['title', 'description'], $search);
    })
    ->orWhereHas('profile', function (Builder $query) use ($search) {
        $query->where('bio', $search);
    })
    ->orWhereHas('profile.address', function (Builder $query) use ($search) {
        $query->where('city', $search);
    })
    ->orWhereHas('orders.shippingAddress', function (Builder $query) use ($search) {
        $query->where('country', $search);
    });

After (with new whereHasAny)

User::query()->whereHasAny([
    'orders.orderItems.product.category' => [ 'name' ],
    'orders.orderItems.product' => [ 'title', 'description' ],
    'profile' => [ 'bio' ],
    'profile.address' => [
      'city', // string
      fn($query) => $query..., // Closure
      DB::raw("..."), // Expression
    ],
    'orders.shippingAddress' => fn($query) => $query..., // Closure
], $search);

Comparison across different relationships

One powerful advantage of whereHasAll is that it allows you to compare the same value across different relationships.

This is extremely useful for real-world scenarios, such as when the same identifier (code, reference, etc.) needs to match across multiple related tables.

For example, suppose a User has both shipments and orders, and you want to find users where both a shipment and an order have the same code:

Before

User::query()
  ->whereHas('shipments', function ($query) use ($search) {
      $query->where('code', $search);
  })
  ->whereHas('orders', function ($query) use ($search) {
      $query->where('code', $search);
  });

After (with new whereHasAll)

User::query()->whereHasAll([
  'shipments' => [ 'code' ],
  'orders' => [ 'code' ],
], $search);

Notes about columns

Internally, the whereHasAny, orWhereHasAny, whereHasAll, and orWhereHasAll methods instantiate their conditions using the existing whereAny and whereAll methods from the Builder class.

$q->whereHasAny([
    'profile' => [
        'name',                    // simple column name
        'profiles.name',            // full table.column to avoid ambiguities
        fn (Builder $query) => $query->one(), // Closure
    ],
    // ...
]);

This approach offers maximum flexibility when filtering nested relationships, supporting direct column names, explicit table.column syntax for disambiguation, and full custom query logic via closures.

Please feel free to share your thoughts — I would love to hear your feedback!

@ezequidias ezequidias marked this pull request as draft April 26, 2025 13:06
Copy link

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.

@browner12
Copy link
Contributor

Gut reaction, the extra code does not feel worth it for the subjectively better readability.

Just turn all your closures into short syntax, and you've got a similar readability for the "before".

@ezequidias
Copy link
Author

Gut reaction, the extra code does not feel worth it for the subjectively better readability.

While I understand the concern that this might seem like extra code for only a subjective readability gain, the purpose of whereHasAny and whereHasAll goes far beyond just syntactic sugar.

This proposal addresses two core improvements in Eloquent:

1. Alias for repetitive whereHas chains:

When dealing with multiple nested relationships, manually chaining whereHas and orWhereHas quickly becomes verbose and hard to maintain.

For example:

User::query()
   ->whereHas('orders.orderItems.product.category', fn($q) => $q)
   ->orWhereHas('orders.orderItems.product', fn($q) => $q);

Can be rewritten more clearly and maintainably as:

User::query()->whereHasAny([
    'orders.orderItems.product.category' => fn($q) => $q,
    'orders.orderItems.product' => fn($q) => $q,
]);

This approach not only reduces code duplication but also streamlines the query construction process. The whereHasAny and whereHasAll methods also accept the $operator and $value arguments as optional!

2. Alias for whereHas + whereAny combined:

Even with short syntax closures, logic like whereAny within nested relationships leads to mental overload. The new methods abstract this boilerplate in a declarative way.
Consider the following example:

User::query()
   ->whereHas('orders.orderItems.product.category', fn ($q) => $q->where('name', $search))
   ->orWhereHas('orders.orderItems.product', fn ($q) => $q->whereAny(['title', 'description'], $search));

With the proposed method, this can be simplified to:

User::query()->whereHasAny([
    'orders.orderItems.product.category' => ['name'],
    'orders.orderItems.product' => ['title', 'description'],
], $search);

Moreover, Laravel has embraced similar improvements before. For instance, PR #55245 introduced a more elegant way to combine whereHas with whereKey, demonstrating that enhancing developer ergonomics—even through minor abstractions—aligns with the framework’s direction.

In practice, this PR promotes clearer intention, improves maintainability for complex queries, and avoids repeated nested closures—especially in real-world applications with deep relationships.

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.

2 participants