Pokus o MultiFactory končí chybou Nette\DI\InvalidConfigurationException

vonTrips
Člen | 5
+
0
-

Řekl jsem si, že bych v novém projektu mohl využít MultiFactory, abych nemusel do fasád nebo presenterů pokaždé přidávat specifickou továrnu, ale přidám si jen tu jednu, která mi „vrátí cokoli“:

  1. Nevím, jestli je to správný přístup? (Že mi továrna vrací vlastní komponenty nebo repositáře)
  2. Můj pokus skončil následující chybou:
Nette\DI\InvalidConfigurationException

Service '08': Method App\Model\MultiFactory::createDbRepository() does not meet the requirements: is create($name), get($name), create*() or get*() and is non-static.

Toto jsou výsledné soubory po prvních pokusech:

services.neon

services:
	- App\Model\Components\UserAddDbForm
	- App\Model\Repository\DbRepository
	- App\Model\MultiFactory(
		userAddDbForm: @App\Model\Components\UserAddDbForm
		dbRepository: @App\Model\Repository\DbRepository
	)

MultiFactory.php

declare(strict_types=1);
namespace App\Model;
use App\Model\Components\UserAddDbForm;
use App\Model\Repository\DbRepository;

interface MultiFactory {
	public function createUserAddDbForm(): UserAddDbForm;
	public function createDbRepository(string $tableName, string $columnId): DbRepository;

}

Když ale zakomentuju řádky s DbRepository v obou souborech, tak kód funguje a továrna vrací komponentu s formulářem. Já nechápu, proč je problém ten repositář a co je tam voláno špatně?

DbRepository.php

declare(strict_types=1);
namespace App\Model\Repository;
use Nette;

final class DbRepository {

	use Nette\SmartObject;

	public function __construct(
					private Nette\Database\Explorer $database,
					private string $tableName,
					private string $columnId
	) {

	}

U repositáře pracuji s tím, že když se vytvoří instance, tak ji předám název tabulky, se kterou má repositář pracovat. U toho $columnId uvažuji, jestli nedat výchozí hodnotu na ‚id‘. Když ale použiju samostatnou továrnu na repositář, tak vše funguje správně.

RepositoryFactory.php

declare(strict_types=1);
namespace App\Model\Repository;
use Nette;
use App\Model\Repository\DbRepository;

interface RepositoryFactory {
	function create(string $tableName, string $columnId): DbRepository;
}
m.brecher
Generous Backer | 765
+
0
-

@vonTrips

Ahoj, s vícenásobnou továrnou zkušenosti nemám a tak jenom komentuji podle dokumentace:

https://doc.nette.org/…tion/factory#…

Druhý příklad kódu odshora, dokumentace: „Alternativně lze místo několika metod použít create() a get() s parameterem:“

ve Tvém kódu je kombinace obojího – createSomething($parameter), dokumentace ale připouští buďto createSomething() nebo create($parameter):

public function createDbRepository(string $tableName, string $columnId): DbRepository;

Zpráva vyhozené výjimky potvrzuje co je v dokumentaci – 4 platné syntaktické varianty create/get metody:

Method App\Model\MultiFactory::createDbRepository() does not meet the requirements: is create($name), get($name), create*() or get*()

Editoval m.brecher (24. 5. 2023 1:33)

vonTrips
Člen | 5
+
0
-

@m.becher

Přiznám se, mám v tom guláš. Ale jestli to chápu správně, tak MultiFactory nemohu použít na vytvoření ničeho, čemu musím předat jakýkoli parametr? A protože DbRepository očekává (alespoň) název tabulky, se kterou má pracovat, musím pro něj využít samostatnou továrnu?

m.brecher
Generous Backer | 765
+
0
-

@vonTrips

Ale jestli to chápu správně, tak MultiFactory nemohu použít na vytvoření ničeho, čemu musím předat jakýkoli parametr?

Včera jsem se sám pokoušel na základě dokumentace vyzkoušet vytvoření objektu nějaké třídy pomocí MultiFactory + metody create() s parametrem a neuspěl jsem. V dokumentaci chybí specifikace toho parametru, ale je tam dost důrazné varování, že u tohoto způsobu lze očekávat nějaké potíže: „Nicméně alternativní zápis má tu nevýhodu, že není zřejmé, jaké hodnoty $name jsou podporované a logicky také nelze v rozhraní odlišit různé návratové hodnoty pro různé $name.“.

Naproti tomu koncept generovaných továren je navržen výborně – pro vytváření komponent presenteru. Každá komponenta má svoji továrnu, lze je proto snadno individuálně upravovat. Je s tím o malinko více psaní, ale funguje to.

U modelových tříd (repository), je otázka, jestli je vytvářet pomocí *Factory. Všeobecně se doporučuje předávat modelové třídy do presenterů/továren komponent autowiringem v konstruktoru. V tomto konceptu modelové objekty vytváří DI Container Nette automaticky a modelové objekty jsou služby, nikoliv komponenty.

Editoval m.brecher (24. 5. 2023 11:45)

m.brecher
Generous Backer | 765
+
0
-

@vonTrips

musím pro něj využít samostatnou továrnu?

Normální Factory bez Nette autogenerování z interface určitě půjde použít. Příklad funkčního kódu:

Obecná modelová třída pro jakoukoliv tabulku:

use Nette\Database\Explorer;
use Nette\Database\Table\Selection;

class TableModel
{
    public readonly string $tableName;
    private Explorer $database;

    public function __construct(string $tableName, Explorer $database)
    {
        $this->tableName = $tableName;
        $this->database = $database;
    }

    public function getAll(): Selection
    {
        return $this->database->table($this->tableName);
    }
}

Factory:

class TableModelFactory
{
    public function __construct(private Explorer $database)
    {}

    public function create(string $tableName): TableModel
    {
        return new TableModel($tableName, $this->database);
    }
}

Použití v presenteru:

use App\Model\TableModelFactory;

final class TestPresenter extends FrontPresenter
{
    public function __construct(
        private TableModelFactory $modelFactory,
    )
    {}

    public function renderDefault()
    {
        $table1 = $this->modelFactory->create('table1');
        $table2 = $this->modelFactory->create('table2');

        $this->template->table1Items = $table1->getAll();
        $this->template->table2Items = $table2->getAll();
    }
}

Tato koncepce vypadá hezky pouze na první pohled. Zásadní nevýhodou je použití jedné třídy pro více tabulek. Praxe vždy přináší různé drobné nebo větší odchylky mezi tabulkami, třídění, filtrování, apod. a potom společná modelová třída bude velkou překážkou pro custom úpravy jednotlivých tabulek. Uvidíš, jak se Ti to v praxi osvědčí.

vonTrips
Člen | 5
+
0
-

m.brecher napsal(a):
Normální Factory bez Nette autogenerování z interface určitě půjde použít.

Tou samostatnou továrnou jsem myslel právě interface, jak jsem jej uvedl v prvním příspěvku (RepositoryFactory.php). S tím, že když budu mít ještě FileRepository, musím i pro tento vytvořit zase samostatnou továrnu (interface)?

Poznámka „bokem“
Ty repositáře mám 3, jen nevím jestli to nazývám správně, možná ne. Celou aplikaci jsem postavil tak, že mám repositáře, které umí pracovat s konkrétním zdrojem (Db nebo File) a celé aplikaci přináší nějaké jednotné funkce nad daným zdrojem. U DB např. získat všechny záznamy, vybrané záznamy, jeden záznam, přidat/editovat/smazat atd.

A pak mám fasády, které vždy pracují s nějakou entitou (názvosloví je asi špatně), třeba s obrázky. U obrázků chci, aby se data ukládala do DB, ale zároveň aby se soubory ukládaly se správným názvem do správných adresářů. Taková fasáda pak využije jak DbRepository tak FileRepository. V rámci DB bude pracovat s tabulkou „images“, zatímco v rámci souborů s adresářem „files/images“. Práci s oběma zdroji pak právě spojím v té fasádě.

Důležité je, že data směrem ven jsou v nějakém základním datovém typu (int, string, array). Takže fasáda bude vždy pracovat nad Repositářem, který ji poskytuje stále stejné metody, bez ohledu na to, jestli je DbRepository postavený na NetteDB, Dibi nebo něčem jiném. Zároveň fasáda, pokud má dodat data, je dodá v základním typu, takže je ji relativně jedno kdo/co s těmi daty dále pracuje.

Moje myšlenka byla, že ta moje aplikační logika bude přenositelná. Že pokud se s Nette úplně neskamarádím a budu chtít využít něco jiného, neměl by to být zásadní problém. Přepíšu jen DbRepository třeba do Laravelu, ale vše ostatní by mělo „hned“ fungovat. Ale možná je už tato myšlenka zcela mimo a já si to dělám zbytečné složitější, než je potřeba…

m.brecher
Generous Backer | 765
+
0
-

@vonTrips

Přepíšu jen DbRepository třeba do Laravelu, ale vše ostatní by mělo “hned” fungovat.

A co přechodem na Laravel získáš ??

Spíš bych doporučil cca jeden/dva roky důvěřovat Nette a psát projekty co nejjednodušším způsobem jen v Nette. Potom to můžeš vyhodnotit. Na první pokus napsat aplikaci přenositelnou mezi frameworky mě přijde nereálné.

Vývojáři Nette běžně po pár letech přidají nějakou další technologii, která v Nette není, typicky třeba Doctrine, nebo nějaké Symfony komponenty. Nebo začnou psát v TypeScriptu/Node.js/React. Přechod na Symfony/Laravel je obvykle dán tím, že získají zakázku, kde je ten jiný framework daný.

Editoval m.brecher (24. 5. 2023 17:40)

Marek Bartoš
Nette Blogger | 1177
+
0
-

Ta multi factory ti maximálně pomůže skrýt, když budeš mít závislostí příliš moc a měl bys je obalit do další služby. Nepoužívej ji, neměla by být potřeba.

Zní to, že repozitářem myslíš totéž, co různá ORM – v tom případě bys měl mít přímo repozitář registrovaný jako službu. Vždy budeš mít pouze jednu instanci repozitáře, nemusíš pokaždé tvořit novou. Pokud chceš repozitář načítat lazy, tak místo factory chceš accessor. Jen změníš create() na get() a bude se ti vracet repozitář z existující služby v DI.

Odstiňovat celou aplikaci od databázové vrstvy je spousta práce navíc. Řekl bych že víc, než za to stojí. Nette nemá entity, Doctrine má hromadu features, ale chová se často dost špatně, Nextras je akademicky čisté a nikdy jsem s ním neměl problém, ale oproti Doctrine nějaké funkce chybí, Eloquent je úzce propojený se zbytkem Laravelu – např. je u nich zvykem ignorovat DI a sahat na závislosti staticky odkudkoli.
Díky specifikům každé vrstvy s abstrakcí nejspíš docílíš jen toho, že knihovnu nikdy nevyužiješ naplno.

Jestli ti jde o čistotu návrhu (i pokud ne), doporučuju Nextras. Zasáhne ti do návrhu nejméně a nebudeš mít důvod měnit.

vonTrips
Člen | 5
+
0
-

m.brecher napsal(a):

A co přechodem na Laravel získáš ??

To byl pouze příklad (proto ono třeba 😉), klidně si za to můžeš dosadit Doctrine nebo cokoli jiného.
Chtěl jsem ještě napsat, že nesouhlasím s druhou částí, že je to otázka návrhu. Ale evidentně je můj návrh zbytečně složitý (možná nepraktický), takže jsem raději ticho. Každopádně díky za zpětnou vazbu.

Marek Bartoš napsal(a):

Ta multi factory ti maximálně pomůže skrýt, když budeš mít závislostí příliš moc a měl bys je obalit do další služby. Nepoužívej ji, neměla by být potřeba.

Aha, potom jsem to celé špatně pochopil a budu vytvářet vždy továrnu pro tu konkrétní komponentu, nebo to co budu potřebovat. Díky

Zní to, že repozitářem myslíš totéž, co různá ORM – v tom případě bys měl mít přímo repozitář registrovaný jako službu. Vždy budeš mít pouze jednu instanci repozitáře, nemusíš pokaždé tvořit novou. Pokud chceš repozitář načítat lazy, tak místo factory chceš accessor. Jen změníš create() na get() a bude se ti vracet repozitář z existující služby v DI.

Asi ano, prostě jsem ten návrh udělal zbytečně složitý. I když mi to přišlo takové hezké, že mám repositář na různé zdroje a ty pak spojím ve fasádě. Ale chápu tedy, že to není úplně správné řešení?

Odstiňovat celou aplikaci od databázové vrstvy je spousta práce navíc. Řekl bych že víc, než za to stojí.

Opět díky za zpětnou vazbu, budu vědět do budoucna. Ale teď už to mám takto navrženo, část je hotová a funguje, takže se mi to nechce zase celé předělávat 😂