RFC – doplnit typovou kontrolu v TemplateFactory ?

m.brecher
Generous Backer | 905
+
0
-

Ahoj,

pokud někomu nevyhovuje vestavěná Nette\Bridges\ApplicationLatte\DefaultTemplate, má možnost si v konfiguraci nastavit vlastní:

latte:
    templateClass: App\Template\MyTemplate

a ve vlastní šabloně si definovat předávané parametry podle vlastního uvážení:

#[AllowDynamicProperties]
class MyTemplate extends Nette\Bridges\ApplicationLatte\Template
{
    public string $myParam;
}

vše funguje do doby, kdy se rozhodneme použít jméno proměnné exitující v built-in šabloně nette, třeba $user ale předat do ní jiný typ např. místo Nette\Security\User nějaké readonly UserDTO. Nette vyhodí TypeError. Výjimku vyhazuje Nette\Bridges\ApplicationLatte\TemplateFactory::createTemplate() zde:

$params = [
    'user' => $this->user,
    'baseUrl' => $baseUrl,
    'basePath' => $baseUrl ? preg_replace('#https?://[^/]+#A', '', $baseUrl) : null,
    'flashes' => $flashes,
    'control' => $control,
    'presenter' => $presenter,
];

foreach ($params as $key => $value) {
    if ($value !== null && property_exists($template, $key)) {
        $template->$key = $value;
    }
}

TemplateFactory předává parametry, jako do DefaultTemplate, kontroluje existenci property a předpokládá stejný typ.

Jak toto omezení custom template (nemožnost definice typu jaký potřebuji) v TemplateFactory vyřešit ?

Ideální by bylo, pokud vývojář použije vlastní template class, nechat injektáž proměnných na něm, ale tato změna v kódu TemplateFactory by znamenala nepříjemný BC break.

Druhou možností je ponechat injekci parametrů jak je a přidat typovou kontrolu tak, aby se zbytečně nesnižoval výkon aplikace.

Zkoušel jsem toto řešení které jednoduchým způsobem problém řeší:

$checkTypes = !is_a($template, DefaultTemplate::class);
if($checkTypes){
    $properties = [];
    $rc = new ReflectionClass($template);
    foreach($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $property){
        $properties[$property->name] = $property;
    }
    foreach ($params as $key => $value) {
        if ($value !== null && isset($properties[$key])) {
            $property = $properties[$key];
            $type = $property->getType();
            if($type instanceof ReflectionNamedType){
                $expected = $type->getName();
                $isValid = $type->isBuiltin() ? match($expected){
                    'string' => is_string($value),
                    'int'    => is_int($value),
                    'bool'   => is_bool($value),
                    'array'  => is_array($value),
                    'float'  => is_float($value),
                    'object' => is_object($value),
                    default  => false,
                }
                : is_a($value, $expected);
                if($isValid){
                    $template->$key = $value;
                }
            }
        }
    }

}else{
    foreach ($params as $key => $value) {
        if ($value !== null && property_exists($template, $key)) {
            $template->$key = $value;
        }
    }
}

//        foreach ($params as $key => $value) {
//            if ($value !== null && property_exists($template, $key)) {
//                $template->$key = $value;
//            }
//        }

Poznámky: ReflectionNamedType zachytí jednoduché + nullable typy, což plně postačuje, pro union typy si vývojář zajistí vlastní injektáž.

Praktické použití: využije se pro definici vlastního objektu $user, pokud vestavěný nette nevyhovuje, v šabloně je ideální mít DTO readonly objekty separované od autentikační logiky.

Dík za případné komentáře. PR: https://github.com/…ion/pull/352

Editoval m.brecher (16. 7. 23:45)