Chyby v konfiguraci – syntaxe property/konstanty v sekci services – aktualizace
- m.brecher
- Generous Backer | 863
Ahoj,
nedávno se ve vlákně zde https://forum.nette.org/…ervices-neon diskutovala matoucí syntaxe @myService::$publicProperty() + chybějící popis v dokumentaci. Na doplnění dokumentace byl podán PR https://github.com/…cs/pull/1065 a vlákno se uzavřelo s tím, že syntaxe se opraví.
Po důkladnějším otestování se ukázalo, že chyb je v konfiguraci několik a také několik chyb v latte pluginu. Proto zakládám toto nové vlákno, jako dokumentaci chyb a podklad pro jejich odstranění.
Testy
Testoval jsem tři prvky:
- public property služby
- public static property služby
- public konstantu služby
a sice čtení z propert ve službě + čtení z property ze služby získané getterem jiné služby.
Testovací třídy:
class Test
{
public function __construct(private readonly string $value)
{}
}
class First
{
public string $property = 'value';
public const string Constant = 'constantValue';
public static string $staticProperty = 'staticValue';
public function __construct(private readonly Second $second)
{}
public function getSecond(): Second
{
return $this->second;
}
}
class Second
{
public string $property = 'value';
public const string Constant = 'constantValue';
public static string $staticProperty = 'staticValue';
}
V konfigračním souboru jsem testoval vždy odezvu nette konfigurace a chování neon pluginu na všech kombinacích správných a nesprávných syntaxí – aby se vychytaly i případy, kdy neplatná syntaxe nevyhazuje výjimku, ale vrací data, která vracet nemá. Celkem tři různé syntaktické prvky testované vždy na třech datových polích v službě a to jak na přímo volané službě, tak i na službě získané gettrem z jiné služby. Celkem tedy 3 × 3 × 2 = 18 testů nette konfigurace a stejný počet pro plugin. Z celkového počtu 36 testů bylo 9 správně a 27 vykazovalo nějakou chybu.
Platná syntaxe
Oficiálně platná syntaxe pro public property, konstanty a public static property je v dokumentaci nekompletní, nicméně dá se logickou úvahou + testováním odvodit, abychom mohli testy vyhodnotit. Předpokládám že platí tato syntaxe:
# public property - povinně první písmeno lowercase
- @service::property
# public constant - povinně první písmeno uppercase
- @service::Constant
# public static property:
- @service::$property
Výsledky testů
Výsledky testů označuji OK nebo NOT_OK podle toho, zda je výsledek v souladu se syntaxí, kterou jsem definoval výše a o které předpokládám, že autoři konfigurace měli v plánu realizovat.
services.neon
services:
first: App\Test\First
second: App\Test\Second
# public non-static property
test:
create: App\Test\Test
arguments:
- @first::property # nette: 'value' - OK
# plugin: warning 'constant not found' - NOT_OK
- @first::$property # nette: 'Expected function, method or property name' - NOT_OK
# plugin: show $nonStaticProperty - NOT_OK
- @first::$property() # nette: 'value' - NOT_OK
# plugin: show $property - NOT_OK
- @first::getSecond()::property # nette: 'Call to undefined method' - NOT_OK
# plugin: warning 'constant not found' - NOT_OK
- @first::getSecond()::$property # nette: 'Access to undeclared static property' - OK
# plugin shows $property - NOT_OK
- @first::getSecond()::$property() # nette: 'Acces to undeclared static property' - NOT_OK
# plugin: shows $nonStaticProperty - NOT_OK
# constant
- @first::Constant # nette: 'constantValue' - OK
# plugin: shows Constant - OK
- @first::$Constant # nette: 'Expected function, method or property name' - NOT_OK
# plugin: 'property $Constant.. not found', wrong message - NOT_OK
- @first::$Constant() # nette: 'Undefined property $Constant' - NOT_OK
# plugin: 'property $Constant.. not found' - NOT_OK
- @first::getSecond()::Constant # nette: 'Call to undefined method' - NOT_OK
# plugin shows Constant - OK
- @first::getSecond()::$Constant # nette: 'Call to undefined method' - NOT_OK
# plugin: 'property Constant not found' - NOT_OK
- @first::getSecond()::$Constant() # nette: 'Access to undeclared static property' - NOT_OK
# plugin: 'property Constant not found' - NOT_OK
# public static property
- @first::staticProperty # nette: 'Accessing static property as non-static' - OK
# plugin: 'constant not found' - NOT_OK
- @first::$staticProperty # nette: 'Expected function, method or property name' - NOT_OK
# plugin shows $staticProperty - OK
- @first::$staticProperty() # nette: 'Accessing static property as non-static' - NOT_OK
# plugin: shows $staticProperty - NOT_OK
- @first::getSecond()::staticProperty # nette: 'Call to undefined method' - NOT_OK
# plugin 'constant not found' - NOT_OK
- @first::getSecond()::$staticProperty # nette: 'staticValue' - OK,
# plugin - OK
- @first::getSecond()::$staticProperty() # nette: 'staticValue' - NOT_OK
# plugin: shows $staticProperty - NOT_OK
Poznámka: dodatečně jsem ještě testoval konstantu + public static property třídy.
Přehled chyb
a) public property služby
- @first::$property() # vrací hodnotu public property, měla by se vyhodit výjimka
- @first::getSecond()::property # volá metodu property(), měla by vrátit 'value'
b) konstanta služby
- @first::getSecond()::Constant # volá metodu Constant(), měla by vrátit 'constantValue'
c) public static property služby
- @first::$staticProperty # by měla vrátit hodnotu, ale vyhodí výjimku
- @first::$staticProperty() # nesprávná výjimku 'Accessing static property as non-static'
- @first::getSecond()::staticProperty # volá metodu staticProperty()
- @first::getSecond()::$staticProperty() # vrací 'staticValue', ale měla by se vyhodit výjimka
d) konstanta třídy
- First::NoneExistentConstant # vrací 'First::NoneExistentConstant', měla by vyhodit výjimku
e) public static property třídy
- First::$staticProperty # vrací 'First::$staticProperty', měla by vrátit 'staticValue'
Přehled správně implementovaných syntaktických prvků
@first::property
@first::Constant
@first::getSecond()::$staticProperty
First::Constant
Shrnutí
Ze tří správně a logicky implementovaných syntaktických prvků pro členy služby/třídy (výše) se dá odvodit, jaká syntaxe je zamýšlená v ostatních případech, kde je aktuálně chaos. Pravidla syntaxe jsou jednoduchá a logická:
- public property a public constant reprezentuje string bez znaku $ na začátku a závorek () na konci,
- property a konstanta se navzájem odliší lowercase/uppercase prvním písmenem.
- public static property začíná znakem $ a nemá na konci závorky ().
- závorky () na konci jména členu jsou vyhrazeny pro metodu/funkci.
- v případě, kdy syntaxe není platná by se měla vyhodit výjimka s relevantní zprávou
Předpokládám, že tato jednoduchá, logická pravidla syntaxe byla v konfiguraci zamýšlena.
Postup
Po případné diskuzi podám na github issue (aby to nezapadlo), odstraní se chyby konfigurace, po opravě konfigurace se opraví chyby v pluginu neon.
Rád si přečtu případné komentáře.
Editoval m.brecher (9. 11. 1:23)
- m.brecher
- Generous Backer | 863
Problém je poměrně komplexní a tak jsem testoval a analyzoval více do hloubky proces kompilace NEON sekce services a pokročil v případném řešení.
Test – konstanty, statické property
Otestujeme získání konstanty a statické property z třídy nebo služby/objektu, kde jsou chyby. Získání property ze služby @provider::property funguje bez chyb. Testujeme službu/třídu Test, definici služby umístíme do samostatného souboru test.neon, aby bylo možno snadno dumpovat procesy při kompilaci DI containeru.
Testovací třída:
class Test
{
public function __construct(
private readonly ?string $val1 = null,
private readonly ?string $val2 = null,
private readonly ?string $val3 = null,
private readonly ?string $val4 = null,
)
{}
Pomocná testovací třída/služba:
class Provider
{
public const string Constant = 'constantValue';
public string $property = 'propertyValue';
public static string $staticProperty = 'staticPropertyValue';
}
Testovací soubor test.neon:
services:
test:
class: App\Test\Test
arguments:
- App\Test\Provider::Constant
- App\Test\Provider::$staticProperty
- @provider::Constant
# - @provider::$staticProperty // dočasně vyřazeno - vyhazuje výjimku
Zkompilovaná služba @test v /temp containeru:
public function createServiceTest(): App\Test\Test
{
return new App\Test\Test(
'constantValue', // chyba a)
'App\Test\Provider::$staticProperty', // chyba b)
App\Test\Provider::Constant, // překvapivé, ale správné
);
}
Chyby:
a) konstanta se nezkompiluje do App\Test\Provider::Constant, ale dosadí se natvrdo její hodnota, to je chyba, protože pokud změníme hodnotu konstanty v třídě Provider, tato změna se v konfiguraci neprojeví a konfigurace dál používá starou neplatnou hodnotu. Takové chyby se hledají obtížně !!
Chybu způsobuje třída NeonAdapter z balíčku nette/neon, v metodě load() se provede kód který hodnotu natvrdo dosadí:
$node = $traverser->traverse($node, $this->resolveConstants(...));
metodu NeonAdapter::resolveConstants() je potřeba odstranit a odstranit její volání a zkompilovat do výsledného containeru původní řádek App\Test\Provider::Constant.
Zakomentujeme problematický řádek v NeonAdapter::load():
// $node = $traverser->traverse($node, $this->resolveConstants(...));
a zkompilovaný kód služby @test obsahuje už jenom chybu b):
public function createServiceTest(): App\Test\Test
{
return new App\Test\Test(
'App\Test\Provider::Constant', // chyba b)
'App\Test\Provider::$staticProperty', // chyba b)
App\Test\Provider::Constant,
);
}
Poznámka: odstraněním metody NeonAdapter::resolveConstants() vyřešíme ještě jinou chybu – metoda detekuje, zda konstanta existuje a pokud neexistuje tak nevyhodí výjimku, ale místo hodnoty konstanty předá kód entity argumentu ‚App\Test\Provider::Constant‘. O překlepu v názvu konstanty se tak nedozvíme a místo výjimky se do služby předá nesprávná hodnota. Vznikne chyba, která se obtížně hledá.
b) první dva argumenty se zkompilují syntakticky správně, ale jako string – stačí tedy v generátoru php kódu vynechat ohraničující apostrofy a máme obě chyby opravené. Vygenerovaný php kód po opravě kompilace by měl vypadat takto:
public function createServiceTest(): App\Test\Test
{
return new App\Test\Test(
App\Test\Provider::Constant,
App\Test\Provider::$staticProperty,
App\Test\Provider::Constant,
);
}
Poznámka: syntaxe @provider::Constant nevygeneruje, co bychom očekávali – volání konstanty na službě:
$this->getService('provider')::Constant,
ale místo služby podstrčí třídu služby:
App\Test\Provider::Constant,
což nevadí, protože funkce je správně.
Test static property služby
upravíme definici testovací třídy:
services:
test:
class: App\Test\Test
arguments:
# - App\Test\Provider::Constant
# - App\Test\Provider::$staticProperty
# - @provider::Constant
- @provider::$staticProperty
Kompilace vyvolá výjimku:
Nette\DI\ServiceCreationException
... Expected function, method or property name, '$$staticProperty' given....
Výjimku vyhazuje třída Resolver v balíčku nette/di v metodě completeStatement() a problematický kód vypadá takto:
case is_array($entity):
if (!preg_match('#^\$?(\\\\?' . PhpHelpers::ReIdentifier . ')+(\[\])?$#D', $entity[1])) {
throw new ServiceCreationException(sprintf(
"Expected function, method or property name, '%s' given.",
$entity[1],
));
}
Zde se validuje syntaxe druhého členu entity <statement>::member ($entity[1]) a z důkladnějších testů vyplývá, že se k hodnotě member položky přidává jako prefix znak $ – ale pouze v případě, že se nejedná o konstantu ani metodu. Takže např. entita @provider::property v tomto místě $entity[1] hodnotu $property, zatímco u @provider::$staticProperty má hodnotu $$staticProperty.
Takže regulární výraz v třídě Resolver musíme upravit, aby syntaxe statické property s přidaným prefixem $$staticProperty prošla validací:
case is_array($entity):
// if (!preg_match('#^\$?(\\\\?' . PhpHelpers::ReIdentifier . ')+(\[\])?$#D', $entity[1])) {
if (!preg_match('#^\$?\$?(\\\\?' . PhpHelpers::ReIdentifier . ')+(\[\])?$#D', $entity[1])) { // povoleno $$
throw new ServiceCreationException(sprintf(
// "Expected function, method or property name, '%s' given.",
"Expected function, method, property or static property name, '%s' given.",
$entity[1],
));
}
Služba @test se zkompiluje takto:
public function createServiceTest(): App\Test\Test
{
return new App\Test\Test($this->getService('provider')->{'$staticProperty'});
}
To je špatně, my potřebujeme vygenerovat tento kód:
public function createServiceTest(): App\Test\Test
{
return new App\Test\Test($this->getService('provider')::$staticProperty);
}
To ale nebude v php generátoru složitá úprava.
Závěr:
Logickými a čitelnými úpravami v NeonAdapter::load() a Resolver::completeStatement() se docílí toho, že do php generátoru kódu se předají syntakticky srozumitelné member položky konstant a static properties, které stačí správně detekovat a správně vygenerovat do php kódu.
Protudovat php generátor služeb jsem zatím neměl čas, ale již nyní je zřejmé, že vyřešení uvedených chyb je na spadnutí. Rád bych znal i názor autora nette/di @DavidGrudl na předložený rozbor a návrh řešení ;).
Poznámka: v konfiguraci služeb jsou ještě další chyby, na kterých pracuji, ale myslím, že lepší postup je soustředit se na tuto skupinu chyb, odstranit je a potom pokračovat na další chyby. Už tak je toto vlákno nepřehledné.
Editoval m.brecher (12. 11. 21:18)