Chyby v konfiguraci – syntaxe property/konstanty v sekci services – aktualizace

m.brecher
Generous Backer | 871
+
+1
-

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
+
+1
-

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
+
+5
-

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)

Felix
Nette Core | 1245
+
+1
-

Musim ocenit, jakou sis s tim dal praci.

m.brecher
Generous Backer | 871
+
+1
-

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
+
+2
-

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
+
+1
-

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)