You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
146 lines
5.2 KiB
146 lines
5.2 KiB
<?php
|
|
|
|
namespace App\UI\Actions;
|
|
|
|
use Illuminate\Support\Facades\Lang;
|
|
use ReflectionClass;
|
|
use ReflectionMethod;
|
|
use ReflectionProperty;
|
|
use Throwable;
|
|
|
|
class ResolveDynamicText
|
|
{
|
|
/**
|
|
* Cache store to prevent duplicate reflection lookups and redundant query executions
|
|
* Structure: [object_hash_or_array_id => [context_key => resolved_object_or_scalar]]
|
|
*/
|
|
private array $resolvedContextCache = [];
|
|
|
|
public function execute(?string $value, array|object $context, ?string $contextTarget = null): ?string
|
|
{
|
|
if (! $value) {
|
|
return null;
|
|
}
|
|
|
|
if (str_contains($value, '.') && Lang::has($value)) {
|
|
return $this->localizeAndBind($value, $context, $contextTarget);
|
|
}
|
|
|
|
return $this->resolveExplicitPlaceholders($value, $context, $contextTarget);
|
|
}
|
|
|
|
private function localizeAndBind(string $key, array|object $data, ?string $contextTarget): string
|
|
{
|
|
$templateString = __($key);
|
|
preg_match_all('/:([a-zA-Z0-9_]+)/', $templateString, $matches);
|
|
|
|
$bindings = [];
|
|
if (! empty($matches[1])) {
|
|
foreach ($matches[1] as $variableName) {
|
|
$targetProperty = $variableName;
|
|
$objectKey = $contextTarget;
|
|
|
|
$bindings[$variableName] = $this->resolveNestedValue(
|
|
$objectKey ?? $variableName,
|
|
$objectKey ? $targetProperty : null,
|
|
$data
|
|
);
|
|
}
|
|
}
|
|
|
|
return __($key, $bindings);
|
|
}
|
|
|
|
private function resolveExplicitPlaceholders(string $text, array|object $data, ?string $contextTarget): string
|
|
{
|
|
preg_match_all('/\{([^}]+)\}/', $text, $matches);
|
|
if (empty($matches[1])) {
|
|
return $text;
|
|
}
|
|
|
|
foreach ($matches[1] as $placeholder) {
|
|
if (str_contains($placeholder, '.')) {
|
|
[$objectName, $property] = explode('.', $placeholder, 2);
|
|
} else {
|
|
$objectName = $contextTarget ?? $placeholder;
|
|
$property = $contextTarget ? $placeholder : null;
|
|
}
|
|
|
|
$replacementValue = $this->resolveNestedValue($objectName, $property, $data);
|
|
$text = str_replace("{{$placeholder}}", $replacementValue, $text);
|
|
}
|
|
|
|
return $text;
|
|
}
|
|
|
|
private function resolveNestedValue(string $objectName, ?string $property, array|object $data): string
|
|
{
|
|
// 1. Generate a unique cache signature for the current payload state
|
|
$contextKey = is_object($data) ? spl_object_hash($data) : md5(serialize($data));
|
|
|
|
// 2. Warm up the context cache exactly once per component/request
|
|
if (! isset($this->resolvedContextCache[$contextKey])) {
|
|
$this->resolvedContextCache[$contextKey] = is_object($data)
|
|
? $this->extractInspectableObjects($data)
|
|
: $data;
|
|
}
|
|
|
|
$cachedContext = $this->resolvedContextCache[$contextKey];
|
|
$target = null;
|
|
|
|
// 3. Look up the cached target context safely without firing new database calls
|
|
if (isset($cachedContext[$objectName])) {
|
|
$target = $cachedContext[$objectName];
|
|
} elseif ($property === null && isset($cachedContext[$objectName]) && is_scalar($cachedContext[$objectName])) {
|
|
return (string) $cachedContext[$objectName];
|
|
}
|
|
|
|
if ($property === null) {
|
|
return $target && is_scalar($target) ? (string) $target : '';
|
|
}
|
|
|
|
if ($target && is_object($target) && isset($target->{$property})) {
|
|
return (string) $target->{$property};
|
|
}
|
|
if ($target && is_array($target) && isset($target[$property])) {
|
|
return (string) $target[$property];
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Reflects and extracts object contexts ONCE, caching the output in memory.
|
|
*/
|
|
private function extractInspectableObjects(object $component): array
|
|
{
|
|
$objects = [];
|
|
$reflection = new ReflectionClass($component);
|
|
|
|
// Scan Public Properties
|
|
foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) {
|
|
$value = $prop->getValue($component);
|
|
$objects[$prop->getName()] = $value;
|
|
}
|
|
|
|
// Scan Public Methods (e.g. Livewire 4 #[Computed] properties)
|
|
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
|
|
if ($method->getNumberOfParameters() === 0 && ! $method->isStatic()) {
|
|
try {
|
|
$name = $method->getName();
|
|
// Ignore internal framework overrides and lifecycle names
|
|
if (! str_starts_with($name, '__') && ! str_starts_with($name, 'get') && ! str_contains($name, 'rendering')) {
|
|
|
|
// 💡 THIS WAS THE CULPRIT: Invoking this on every string match triggered query loops.
|
|
// Now it runs exactly once per request render pass, and the model is stored in memory.
|
|
$objects[$name] = $component->{$name};
|
|
}
|
|
} catch (Throwable $e) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $objects;
|
|
}
|
|
}
|
|
|