Chyby v konfiguraci – syntaxe property/konstanty v sekci services – aktualizace
- m.brecher
- Generous Backer | 871
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 | 871
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)
- m.brecher
- Generous Backer | 871
Vyřešeno
Chyby v syntaxi property/konstanty v argumentech služeb a položkách setup jsem vyřešil a otestoval. Řešení zahrnuje úpravy osmi tříd ve třech balíčcích nette. Podal jsem PR, který se skládá ze tří dílčích PR do každého balíčku jeden:
PR 1 – nette/di https://github.com/…/di/pull/318
PR 2 – nette/neon https://github.com/…neon/pull/73
PR 3 – nette/application https://github.com/…ion/pull/339
První skupina chyb je v těchto syntaxích statement prvků v argumentech/setup
arguments:
- Class::Constant
- Class::NonExistedConstant
- Class::$staticProperty
- @service::$staticProperty
Tyto chyby se odstranily úpravou tříd NeonAdapter a Resolver
Druhá skupina chyb je splynutí konstant/property s metodou na konci getter řetězce:
- @service::getObject()::Constant // volá metodu Constant()
- @service::getObject()::property // volá metodu property()
Třetí skupina chyb je nedokončené parsování kombinovaných řetězců member v statementu, kdy řetězec není složen jen z getMethod(), ale obsahuje alespoň jednu property (ne na konci):
- @service::getObject()::property::getValue() // výjimka
- @service::getObject()::$staticProperty::getValue() // výjimka
- @service::property::getObject()::getValue() // výjimka
Parser totiž rozděluje řetězec statement podle oddělovače ‚()‘, nikoliv podle oddělovače ‚::‘. Při odstranění chyb ze druhé a třetí skupiny se nedalo vyhnout určitým zásahům do okolního kódu, tyto změny jsem ale minimalizoval. Závěrečné testy žádné problémy ve frameworku nezjistily.
V principu není fix těchto chyb složitý. Ty části statement chain, které se nezparsovaly v Parseru se zparsují dodatečně a informaci u koncového členu member zda se jedná o metodu nebo uložíme v Parseru do member stringu na konec jako (). Pozměníme syntaxi metody, konstanty, property a statické property aby se daly bezpečně odlišit a přizpůsobíme tomu okolní kód – různé validace apod… I když to vypadá složitě, dá se to zvládnout. Musí se upravit jedna definice služby v ApplicationExtension, úpravy validací syntaxe v Resolveru a drobná úprava v PhpGenerator.
Zpracování kompilace statementů není v DI přímočaré, provádí se více větvemi na více místech. Např. @service::Constant se transformuje na Reference až v Resolveru, zatímco @service::getValue() již v Parseru. Je to zřejmě dáno tím, jak se postupně přidávaly feature. Tomu odpovídají relativně složité úpravy v ServiceDefinition, kde jsem funkční algoritmus pro doparsování + převod syntaxe ladil podle vstupu/výstupu, protože logickou analýzou to nešlo.
Přehled syntaxí member členů v DI před a po úpravě:
Výstup Parseru – původní syntaxe:
Constant
property
$staticProperty
getMethod
Výstup Parseru – syntaxe po úpravě:
Constant
property
$staticProperty
getMethod() // přidání suffixu
Vstup PhpGeneratoru – původní syntaxe:
Constant
$property
$$staticProperty
getMethod
Vstup PhpGeneratoru – syntaxe po úpravě:
$Constant // přidán prefix
$property
$$staticProperty // přidána podpora vygenerování
getMethod
V DI se převádí syntaxe member členů ze syntaxe na výstupu Parseru na syntaxi na vstupu PhpGenerátoru. To se po úpravě provádí v ServiceDefinition třídě současně s parsováním nezparsovaných chain.
Testy
services:
test:
class: Test\Test
arguments:
# class
- Test\First::Constant
- Test\First::$staticProperty
- ::constant('GlobalConstant')
# service
- @first::Constant
- @first::property
- @first::$staticProperty
- @first::getValue()
# simple chains
- @first::second::Constant
- @first::second::property
- @first::second::$staticProperty
- @first::second::getValue()
- @first::getSecond()::Constant
- @first::getSecond()::property
- @first::getSecond()::$staticProperty
- @first::getSecond()::getValue()
- @first::$secondByStatic::Constant
- @first::$secondByStatic::property
- @first::$secondByStatic::$staticProperty
- @first::$secondByStatic::getValue()
# complex chains
- @first::getSecond()::getThird()::Constant
- @first::getSecond()::getThird()::property
- @first::getSecond()::getThird()::$staticProperty
- @first::getSecond()::getThird()::getValue()
- @first::second::getThird()::Constant
- @first::second::getThird()::property
- @first::second::getThird()::$staticProperty
- @first::second::getThird()::getValue()
- @first::second::third::Constant
- @first::second::third::property
- @first::second::third::$staticProperty
- @first::second::third::getValue()
- @first::getSecond()::third::Constant
- @first::getSecond()::third::property
- @first::getSecond()::third::$staticProperty
- @first::getSecond()::third::getValue()
- @first::getSecond()::getValueByStatic()
- @first::$secondByStatic::getThird()::getValue()
- @first::$secondByStatic::third::property
- @first::$secondByStatic::$thirdByStatic::$staticProperty
# setup
setup:
- $prop1 = @first::getSecond()::getThird()::getValue()
- $prop2 = @first::second::third::property
- $prop3 = @first::$secondByStatic::getThird()::getValue()
- setProperty(@first::$secondByStatic::getThird()::getValue())
Bez chyb!
Editoval m.brecher (24. 11. 15:39)
- m.brecher
- Generous Backer | 871
Další testy – dev verze ?
Bylo by dobré vzhledem k rozsahu změn v kódu provést nějaké důkladnější testování. Ale jak, když je to v několika balíčcích ? Co takhle, kdyby @DavidGrudl mergnul moje PR do nějakých dev verzí, kterou by si mohlo více testerů stáhnout a otestovat ve svých projektech, jestli jim něco nespadne.
Já jsem svoje testy dal na github zde: https://github.com/…-syntax-test
Editoval m.brecher (26. 11. 23:19)
- m.brecher
- Generous Backer | 871
Odsouhlasení syntaxe member členů Statement
Pokud by komunita + @DavidGrudl nic proti navržené syntaxi nenamítal, mohl bych již nyní připravit podklady pro @mesour pro opravu chyb v NEON Pro pluginu pro PhpStorm. Práce na DI a pluginu mohou probíhat současně.
Syntaxe member členů je stručně řečeno tato:
::Constant # konstanta - first character uppercase [A-Z]
::property # property - first character lowercase [a-z]
::$staticProperty # static property - first character $, second character [a-zA-Z_]
::method() # first character [a-zA-Z], ends with () or (statement)
Konstanty a statické properties lze použít (doporučovaný postup) na třídě:
MyClass::Constant # konstanta - first character uppercase [A-Z]
MyClass::$staticProperty # static property - first character $, second character [a-zA-Z_]
Všechny member členy lze použít na službě:
@someService::Constant
@someService::$staticProperty
@someService::property
@someService::getValue()
Member členy lze libovolně řetězit za sebou, konstanta může být pouze na konci:
@someService::someObject::Constant
@someService::someObject::$staticProperty
@someService::someObject::anotherObject::property
@someService::getSomeObject()::property
@someService::getSomeObject()::getAnotherObject()::getValue()
@someService::$someObject::getAnotherObject()::getValue()
Poznámka: Použití statických properties na instancích tříd všeobecně není doporučovaný postup a měly by se používat na třídě. PHP nicméně použití statických properties na instanci třídy umožňuje a proto není důvod, aby neon syntax toto nepodporovala. Práce navíc se statickými properties je zanedbatelná.
- Marek Bartoš
- Nette Blogger | 1274
Na tohle jsem narážel od samého začátku a i teď, když to občas potřebuju, tak koukám do testů jak to vůbec napsat správně. Díky za to, že to řešíš.
Byl bych pro to, aby statické a nestatické properties měly stejnou
syntaxi – $property
. Zda ji volat staticky nebo nestaticky lze
zjistit přes reflexi. (snad to nebude velký problém, v tomhle okamžiku už
potřebuješ vědět, jakého typu služba je, abys reflexi vytvořil)
Bude tak na první pohled jasné že jde o property a není třeba tak
vycházet z předpokladu že všichni používají pro názvy konstant a
properties stejnou konvenci.
Pokud property neexistuje, tak výchozí je nestatická, kvůli magickému
__set()
– to pro statiku totiž neexistuje.
Co mi v tvém návrhu chybí, je zpětná kompatibilita.
Např. pokud do teď zápis volal metodu a nově by mělo jít o property, tak
se ta metoda pořád musí zavolat, ve dvou případech:
- existuje metoda, ale neexistuje property – vyvolá se warning vysvětlující, jak syntaxi změnit a upozorňující, že se v další verzi beze změny použije property
- existuje odpovídající metoda i property – stejné řešení, s navrhovanou změnou s $ pro property lze i se zpětnou kompatibilitou metodu od property jasně odlišit
Teprve s příští major verzí nette/di je možné staré chování odstranit. Jinak to způsobí víc škody než užitku.
Editoval Marek Bartoš (28. 11. 13:49)