Skip to content

Commit 4996f22

Browse files
authored
Core: Fix logic for get magic getters.
1 parent f462592 commit 4996f22

File tree

1 file changed

+112
-44
lines changed

1 file changed

+112
-44
lines changed

src/Core/Core.php

Lines changed: 112 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,22 @@ public function __construct(
2222

2323

2424
/**
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>
2831
*/
2932
public function processCandidateSearch(string $query, string $entity, array $columns, array $userConditions): array
3033
{
31-
$return = [];
3234
$columnGetters = $this->getColumnGetters($columns);
3335
$query = strtolower(trim(Strings::toAscii($query)));
3436

3537
/** @var object[] $candidateResults */
3638
$candidateResults = $this->queryBuilder->build($query, $entity, $columns, $userConditions)->getQuery()->getResult();
3739

40+
$return = [];
3841
foreach ($candidateResults as $candidateResult) {
3942
$finalScore = 0;
4043
$snippets = [];
@@ -44,61 +47,72 @@ public function processCandidateSearch(string $query, string $entity, array $col
4447
if ($mode === '_') {
4548
continue;
4649
}
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);
4953
} else { // scalar field
50-
$methodName = 'get' . $columnGetters[$column];
54+
$getter = 'get' . $getterColumn;
5155
$candidateResultClass = $candidateResult::class;
5256
$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);
5560
foreach ($methodRef->getParameters() as $parameter) {
5661
if ($parameter->isOptional() === false) {
5762
$emptyRequiredParameters = false;
5863
break;
5964
}
6065
}
66+
$methodExist = true;
6167
} catch (\ReflectionException) {
6268
// Silence is golden.
6369
}
6470

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
6693
try {
67-
$propertyRef = new \ReflectionProperty($candidateResultClass, Strings::firstLower($columnGetters[$column]));
94+
$propertyRef = new \ReflectionProperty($candidateResultClass, Strings::firstLower($getterColumn));
6895
$propertyRef->setAccessible(true);
6996
$columnDatabaseValue = $propertyRef->getValue($candidateResult);
7097
} catch (\ReflectionException $e) {
7198
throw new \RuntimeException('Can not read property "' . $column . '" from "' . $candidateResultClass . '": ' . $e->getMessage(), $e->getCode(), $e);
7299
}
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);
85105
}
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;
93106
} 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) {
94112
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(),
102116
);
103117
}
104118
}
@@ -134,8 +148,36 @@ public function processCandidateSearch(string $query, string $entity, array $col
134148

135149

136150
/**
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>
139181
*/
140182
private function getColumnGetters(array $columns): array
141183
{
@@ -220,19 +262,45 @@ private function getValueByRelation(string $column, ?object $candidateEntity = n
220262
/**
221263
* @return array<int, string>
222264
*/
223-
private function getMagicGettersByClass(string $class): array
265+
private function getMagicMethodsByClass(object $entity, ?string $prefix = null): array
224266
{
267+
/** @var array<class-string, array<int, string>> $cache */
225268
static $cache = [];
269+
$class = $entity::class;
226270
if (class_exists($class) === false) {
227271
throw new \InvalidArgumentException('Class "' . $class . '" does not exist.');
228272
}
229273
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] = [];
233279
}
234280
}
235281

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];
237305
}
238306
}

0 commit comments

Comments
 (0)