@@ -22,19 +22,22 @@ public function __construct(
22
22
23
23
24
24
/**
25
- * @param string[] $columns
26
- * @param string[] $userConditions
27
- * @return SearchItem[]
25
+ * Based on the user query, the database entity, and the constraints, this method finds a list of candidate results.
26
+ * The candidates can be sorted in any order regardless of relevance.
27
+ *
28
+ * @param array<int, string> $columns
29
+ * @param array<int, string> $userConditions
30
+ * @return array<int, SearchItem>
28
31
*/
29
32
public function processCandidateSearch (string $ query , string $ entity , array $ columns , array $ userConditions ): array
30
33
{
31
- $ return = [];
32
34
$ columnGetters = $ this ->getColumnGetters ($ columns );
33
35
$ query = strtolower (trim (Strings::toAscii ($ query )));
34
36
35
37
/** @var object[] $candidateResults */
36
38
$ candidateResults = $ this ->queryBuilder ->build ($ query , $ entity , $ columns , $ userConditions )->getQuery ()->getResult ();
37
39
40
+ $ return = [];
38
41
foreach ($ candidateResults as $ candidateResult ) {
39
42
$ finalScore = 0 ;
40
43
$ snippets = [];
@@ -44,61 +47,72 @@ public function processCandidateSearch(string $query, string $entity, array $col
44
47
if ($ mode === '_ ' ) {
45
48
continue ;
46
49
}
47
- if (str_contains ($ columnGetters [$ column ], '. ' ) === true ) { // relation
48
- $ rawColumnValue = $ this ->getValueByRelation ($ columnGetters [$ column ], $ candidateResult );
50
+ $ getterColumn = $ columnGetters [$ column ];
51
+ if (str_contains ($ getterColumn , '. ' ) === true ) { // relation
52
+ $ rawColumnValue = $ this ->getValueByRelation ($ getterColumn , $ candidateResult );
49
53
} else { // scalar field
50
- $ methodName = 'get ' . $ columnGetters [ $ column ] ;
54
+ $ getter = 'get ' . $ getterColumn ;
51
55
$ candidateResultClass = $ candidateResult ::class;
52
56
$ emptyRequiredParameters = true ;
53
- try {
54
- $ methodRef = new \ReflectionMethod ($ candidateResultClass , $ methodName );
57
+ $ methodExist = false ;
58
+ try { // is available by getter?
59
+ $ methodRef = new \ReflectionMethod ($ candidateResultClass , $ getter );
55
60
foreach ($ methodRef ->getParameters () as $ parameter ) {
56
61
if ($ parameter ->isOptional () === false ) {
57
62
$ emptyRequiredParameters = false ;
58
63
break ;
59
64
}
60
65
}
66
+ $ methodExist = true ;
61
67
} catch (\ReflectionException ) {
62
68
// Silence is golden.
63
69
}
64
70
65
- if ($ emptyRequiredParameters === false ) { // Use property loading if method can not be called
71
+ if ($ methodExist === false ) { // method does not exist, but it is a magic?
72
+ $ columnDatabaseValue = null ;
73
+ $ magicGetters = $ this ->getMagicMethodsByClass ($ candidateResult , 'get ' );
74
+ if (in_array ($ getter , $ magicGetters , true )) {
75
+ try {
76
+ // Get data from magic getter
77
+ $ columnDatabaseValue = $ candidateResult ->$ getter ();
78
+ } catch (\Throwable ) {
79
+ // Silence is golden.
80
+ }
81
+ } elseif ($ magicGetters === []) {
82
+ throw new \InvalidArgumentException (
83
+ 'There are no magic getters in the " ' . $ entity . '" entity, '
84
+ . 'but getter " ' . $ getter . '" is mandatory. ' ,
85
+ );
86
+ } else {
87
+ throw new \InvalidArgumentException (
88
+ 'Getter " ' . $ getter . '" in entity " ' . $ entity . '" does not exist. '
89
+ . 'Did you mean " ' . implode ('", " ' , $ magicGetters ) . '"? ' ,
90
+ );
91
+ }
92
+ } elseif ($ emptyRequiredParameters === false ) { // Use property loading if method can not be called
66
93
try {
67
- $ propertyRef = new \ReflectionProperty ($ candidateResultClass , Strings::firstLower ($ columnGetters [ $ column ] ));
94
+ $ propertyRef = new \ReflectionProperty ($ candidateResultClass , Strings::firstLower ($ getterColumn ));
68
95
$ propertyRef ->setAccessible (true );
69
96
$ columnDatabaseValue = $ propertyRef ->getValue ($ candidateResult );
70
97
} catch (\ReflectionException $ e ) {
71
98
throw new \RuntimeException ('Can not read property " ' . $ column . '" from " ' . $ candidateResultClass . '": ' . $ e ->getMessage (), $ e ->getCode (), $ e );
72
99
}
73
- } else { // Call native method when contain only optional parameters
74
- if (isset ($ methodRef )) {
75
- try {
76
- $ columnDatabaseValue = $ methodRef ->invoke ($ candidateResult );
77
- } catch (\ReflectionException $ e ) {
78
- throw new \LogicException ($ e ->getMessage (), $ e ->getCode (), $ e );
79
- }
80
- } elseif (in_array ($ methodName , $ this ->getMagicGettersByClass ($ candidateResultClass ), true )) {
81
- /** @phpstan-ignore-next-line */
82
- $ columnDatabaseValue = $ candidateResult ->$ methodName ();
83
- } else {
84
- throw new \LogicException ('Method " ' . $ methodName . '" can not be called on " ' . $ candidateResultClass . '". ' );
100
+ } elseif (isset ($ methodRef )) { // Call native method when contain only optional parameters
101
+ try {
102
+ $ columnDatabaseValue = $ methodRef ->invoke ($ candidateResult );
103
+ } catch (\ReflectionException $ e ) {
104
+ throw new \LogicException ($ e ->getMessage (), $ e ->getCode (), $ e );
85
105
}
86
- }
87
- if (is_array ($ columnDatabaseValue ) === true ) {
88
- $ rawColumnValue = implode (', ' , $ columnDatabaseValue );
89
- } elseif (is_scalar ($ columnDatabaseValue ) === true || $ columnDatabaseValue === null ) {
90
- $ rawColumnValue = (string ) $ columnDatabaseValue ;
91
- } elseif (is_object ($ columnDatabaseValue ) && method_exists ($ columnDatabaseValue , '__toString ' )) {
92
- $ rawColumnValue = (string ) $ columnDatabaseValue ;
93
106
} else {
107
+ throw new \LogicException ('Method " ' . $ getter . '" can not be called on " ' . $ candidateResultClass . '". ' );
108
+ }
109
+ try {
110
+ $ rawColumnValue = $ this ->hydrateColumnValue ($ columnDatabaseValue );
111
+ } catch (\InvalidArgumentException $ e ) {
94
112
throw new \InvalidArgumentException (
95
- 'Column definition error: '
96
- . 'Column " ' . ($ columnGetters [$ column ] ?? $ column ) . '" of entity " ' . $ entity . '" '
97
- . 'can not be converted to string because the value is not scalar type. '
98
- . (is_object ($ columnDatabaseValue )
99
- ? 'Object type of " ' . $ columnDatabaseValue ::class . '" '
100
- : 'Type " ' . \get_debug_type ($ columnDatabaseValue ) . '" ' )
101
- . ' given. Did you mean to use a relation with dot syntax like "relation.targetScalarColumn"? ' ,
113
+ 'Column " ' . ($ getterColumn ?? $ column ) . '" of entity " ' . $ entity . '" '
114
+ . 'can not be converted to string because the value is not scalar type. ' . "\n"
115
+ . 'Advance info: ' . $ e ->getMessage (),
102
116
);
103
117
}
104
118
}
@@ -134,8 +148,36 @@ public function processCandidateSearch(string $query, string $entity, array $col
134
148
135
149
136
150
/**
137
- * @param string[] $columns
138
- * @return string[]
151
+ * Translate getter value to scalar
152
+ */
153
+ private function hydrateColumnValue (mixed $ haystack ): string
154
+ {
155
+ if (is_array ($ haystack )) {
156
+ return implode (', ' , $ haystack );
157
+ }
158
+ if (is_scalar ($ haystack ) || $ haystack === null ) {
159
+ return (string ) $ haystack ;
160
+ }
161
+ if (
162
+ is_object ($ haystack )
163
+ && ($ haystack instanceof \Stringable || method_exists ($ haystack , '__toString ' ))
164
+ ) {
165
+ return (string ) $ haystack ;
166
+ }
167
+
168
+ throw new \InvalidArgumentException (
169
+ 'Entity column can not be converted to string. '
170
+ . (is_object ($ haystack )
171
+ ? 'Object type of " ' . $ haystack ::class . '" '
172
+ : 'Type " ' . \get_debug_type ($ haystack ) . '" ' )
173
+ . ' given. Did you mean to use a relation with dot syntax like "relation.targetScalarColumn"? ' ,
174
+ );
175
+ }
176
+
177
+
178
+ /**
179
+ * @param array<int, string> $columns
180
+ * @return array<string, string>
139
181
*/
140
182
private function getColumnGetters (array $ columns ): array
141
183
{
@@ -220,19 +262,45 @@ private function getValueByRelation(string $column, ?object $candidateEntity = n
220
262
/**
221
263
* @return array<int, string>
222
264
*/
223
- private function getMagicGettersByClass ( string $ class ): array
265
+ private function getMagicMethodsByClass ( object $ entity , ? string $ prefix = null ): array
224
266
{
267
+ /** @var array<class-string, array<int, string>> $cache */
225
268
static $ cache = [];
269
+ $ class = $ entity ::class;
226
270
if (class_exists ($ class ) === false ) {
227
271
throw new \InvalidArgumentException ('Class " ' . $ class . '" does not exist. ' );
228
272
}
229
273
if (isset ($ cache [$ class ]) === false ) {
230
- $ classRef = new \ReflectionClass ($ class );
231
- if (preg_match_all ('~@method\s+(\S+\s+)?(get\w+)\(~ ' , (string ) $ classRef ->getDocComment (), $ parser ) > 0 ) {
232
- $ cache [$ class ] = $ parser [2 ] ?? [];
274
+ $ classRef = $ this ->getReflection ($ entity );
275
+ if (preg_match_all ('~@method\s+(?:\S+\s+)?(\w+)\(~ ' , (string ) $ classRef ->getDocComment (), $ parser ) > 0 ) {
276
+ $ cache [$ class ] = $ parser [1 ] ?? [];
277
+ } else {
278
+ $ cache [$ class ] = [];
233
279
}
234
280
}
235
281
236
- return $ cache [$ class ] ?? [];
282
+ $ return = [];
283
+ foreach ($ cache [$ class ] as $ method ) {
284
+ if ($ prefix === null || str_starts_with ($ method , $ prefix )) {
285
+ $ return [] = $ method ;
286
+ }
287
+ }
288
+
289
+ return $ return ;
290
+ }
291
+
292
+
293
+ private function getReflection (object $ entity ): \ReflectionClass
294
+ {
295
+ static $ cache = [];
296
+ $ class = $ entity ::class;
297
+ if (class_exists ($ class ) === false ) {
298
+ throw new \InvalidArgumentException ('Class " ' . $ class . '" does not exist. ' );
299
+ }
300
+ if (isset ($ cache [$ class ]) === false ) {
301
+ $ cache [$ class ] = new \ReflectionClass ($ class );
302
+ }
303
+
304
+ return $ cache [$ class ];
237
305
}
238
306
}
0 commit comments