66
77use Closure ;
88use Illuminate \Support \Str ;
9+ use PDPhilip \Elasticsearch \Exceptions \BuilderException ;
910use PDPhilip \Elasticsearch \Exceptions \RuntimeException ;
11+ use PDPhilip \Elasticsearch \Query \Builder ;
1012
1113/**
1214 * Nested object and parent/child relationship queries.
1315 * For when your docs have docs inside docs.
1416 */
1517trait BuildsNestedQueries
1618{
19+ /**
20+ * Return a Builder pre-configured for a nested path.
21+ *
22+ * Use this when you need to build the inner nested query programmatically
23+ * (across functions, conditionals, loops) rather than inside a closure.
24+ * The returned Builder can be passed straight to whereNestedObject():
25+ *
26+ * $inner = BlogPost::nestedQuery('comments');
27+ * $inner->where('country', 'Peru');
28+ * if ($minLikes) $inner->where('likes', '>=', $minLikes);
29+ *
30+ * BlogPost::whereNestedObject('comments', $inner)->get();
31+ */
32+ public function nestedQuery (string $ column ): Builder
33+ {
34+ $ query = $ this ->newQuery ($ this ->from );
35+ $ query ->options ()->add ('parentField ' , $ column );
36+
37+ return $ query ;
38+ }
39+
1740 /**
1841 * Query nested objects within a document.
1942 * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-nested-query.html
43+ *
44+ * The $query argument may be a Closure (most common), a pre-built Builder
45+ * returned by nestedQuery(), or a raw DSL string.
2046 */
2147 public function whereNestedObject ($ column , $ query , $ filterInnerHits = false , $ options = [], $ boolean = 'and ' , $ not = false ): self
2248 {
@@ -28,16 +54,39 @@ public function whereNestedObject($column, $query, $filterInnerHits = false, $op
2854 }
2955 $ options = $ options ->toArray ();
3056
31- if (! is_string ($ query ) && is_callable ($ query )) {
57+ $ query = $ this ->resolveNestedQuery ($ column , $ query );
58+
59+ $ this ->wheres [] = compact ('column ' , 'query ' , 'type ' , 'boolean ' , 'not ' , 'options ' );
60+
61+ return $ this ;
62+ }
63+
64+ /**
65+ * Normalize the $query argument accepted by whereNestedObject / filterNested.
66+ * Accepts a Closure, a pre-built Builder, or a raw DSL string.
67+ */
68+ protected function resolveNestedQuery (string $ column , mixed $ query ): mixed
69+ {
70+ if ($ query instanceof Builder) {
71+ return $ query ;
72+ }
73+
74+ if (is_string ($ query )) {
75+ return $ query ;
76+ }
77+
78+ if (is_callable ($ query )) {
3279 $ callback = $ query ;
33- $ query = $ this ->newQuery ($ from );
80+ $ query = $ this ->newQuery ($ this -> from );
3481 $ query ->options ()->add ('parentField ' , $ column );
3582 call_user_func ($ callback , $ query );
36- }
3783
38- $ this ->wheres [] = compact ('column ' , 'query ' , 'type ' , 'boolean ' , 'not ' , 'options ' );
84+ return $ query ;
85+ }
3986
40- return $ this ;
87+ throw new BuilderException (
88+ 'Nested query must be a Closure, a Builder from nestedQuery(), or a raw DSL string. Got ' .get_debug_type ($ query ).'. '
89+ );
4190 }
4291
4392 public function orWhereNestedObject ($ column , $ query , $ filterInnerHits = false , $ options = []): self
@@ -74,12 +123,7 @@ public function filterNested($column, $query, $options = [])
74123 $ options = $ this ->setOptions ($ options , 'nested ' );
75124 $ options = $ options ->toArray ();
76125
77- if (! is_string ($ query ) && is_callable ($ query )) {
78- $ callback = $ query ;
79- $ query = $ this ->newQuery ($ from );
80- $ query ->options ()->add ('parentField ' , $ column );
81- call_user_func ($ callback , $ query );
82- }
126+ $ query = $ this ->resolveNestedQuery ($ column , $ query );
83127
84128 $ this ->wheres [] = compact ('column ' , 'query ' , 'type ' , 'boolean ' , 'not ' , 'options ' );
85129
0 commit comments