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

m.brecher
Generous Backer | 863
+
0
-

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

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)