Best practice: komponenty a struktura projektu

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
tolljump
Člen | 47
+
0
-

Ahojte.

Verim tomu ze tato tema sa zvrtne na rozsiehalejsiu diskusiu a nam to vyriesi dlhodobu otazku pri rieseni co najlepsich postupov v budovani projektu väcsieho rozsahu.

Co by som sa chcel poradit minimalne na tieto hlavne otazky:

  1. Ako strukturovat komponenty
  2. Ako komponentam spravne predavat zavislosti
  3. Ako komponenty dostat do presentra a inych komponent

Skusim jednoducho opisat nasu aktualnu situaciu:

Mame adresar „components“ v ktorom je zatial cca. 25 komponent. Budeme ich neskor triedit o nieco lepsie.
Kazda komponenta sa sklada minimalne z vlastneho Control, ControlFactory a .latte sablony.
Cize napr. AddToNewsletterControl.php, AddToNewsletterControlFactory.php a AddToNewsletterControl.latte

Factory zaregistrujeme do configu pod services.
Cize napr.:

  • App\Components\AddToNewsletterControlFactory

Nasledne mame vytvoreny trait v adresari „components“ ktoru sme „laicky“ pomenovali LoadFactories.php
Tento subor obsahuje funkcie pre vytvaranie vsetkych komponent (je to vykonovo idealne? spravne riesenie?):

// ADD TO NEWSLETTER
	private $addToNewsletterControlFactory;

	public function injectAddToNewsletterControlFactory(\App\Components\AddToNewsletterControlFactory $factory){
		$this->addToNewsletterControlFactory = $factory;
	}
	protected function createComponentAddToNewsletter(){
		$componentsSettings = $this->componentsSettings['addToNewsletter'];
		return $this->addToNewsletterControlFactory->create($componentsSettings);
	}

Tento trait mame pouzity v BasePresenter-i pomocou use. Vhodne riesenie nacitania?

K prvej otazke:
je to spravna struktura a nacitavanie komponent pre presenter?
mohli by sme vytvorit nejaku BaseComponent-u od ktorej by dedili vsetky komponenty a nacitaval by sa do nej ten isty trait ako v presentri?
Ako inak dostat vzdy vsetky komponenty navzajom do akejkolvek dalsej komponenty?

K druhej otazke:
Takmer kazdej komponente predavame z presentra premennu $componentsSettings. Nieje to nic ine, ako pole s nastaveniami komponent vytiahnute z databazy. Je trochu strukturovane podla nazvu komponenty a jej pozicie.
Myslim si ze to je škarede riesenie ako predavat kazdej komponente toto $componentsSettings. Neislo by to nejakou triedou ktoru si injectnem do kazdej komponenty, aj do presentrov? Tak aby sa ten dotaz do databazy vykonal len raz.
Nasledne vsetky repositare a ine zavislosti predavame pomocou Factory a jeho constructora v danej komponente. Napr.:

class AddToNewsletterControlFactory {

	private $userRepository,
			$mailingNewsletterEmailRepository;

	public function __construct(
		Model\User\UserRepository $userRepository,
		Model\Mailing\MailingNewsletterEmailRepository $mailingNewsletterEmailRepository
	){
		$this->userRepository = $userRepository;
		$this->mailingNewsletterEmailRepository = $mailingNewsletterEmailRepository;
	}

	/** @return AddToNewsletterControl */
	public function create($componentSettings){
		return new AddToNewsletterControl(
			$componentSettings,
			$this->userRepository,
			$this->mailingNewsletterEmailRepository
		);
	}
}

Ako vidite, zase je tam ta hnusna premenna $componentSettings. Chcel by som sa jej nejako zbavit a riesit ju az priamo v komponente z nejakeho objektu ktory sa vytvori len raz a pouziva sa pre vsetky komponenty, presenter, atd…

A k tretej otazke:
To som uz zacal vyssie. Mame vsetky komponenty definovane v trait LoadFactories ktory pomocou use dostavame do BasePresentra, cize vieme vsetky komponenty pouzivat vo vsetkych presentroch.
Avsak teraz potrebujeme dostavat este aj komponenty do komponent a preto sa pytam, ci je vhodne riesenie znova tento trait hodit do nejakej BaseComponent od ktorej by dedili vsetky komponenty, alebo je na to nejake krajsie a „programatorskejsie“ riesenie.

Trochu som sa rozpisal, ale dufam ze tymto vyriesime niekolko, aj zaciatocnickych otazok, definitivne.

Dakujem za akekolvek tipy, zdroje, navody, postrehy. Dufam ze to pomoze viacerym k budovaniu peknych a uspesnych projektov v ktorych sa po rokoch nebudu stracat ;-)

jiri.pudil
Nette Blogger | 1029
+
+1
-

Ad 1, 3: podívej se na autowired component factories v Kdyby/Autowired. Není to sice zrovna čisté řešení, ale je strašně pohodlné a jednoduché na použití.

Ad 2: obalit si tu konfiguraci do služby je určitě dobrý nápad.

Bonus: továrničky, které vypadají tak jako ta výše, se dají zjednodušit na interface, Nette pak implementaci podobnou té nahoře vygeneruje za tebe:

interface AddToNewsletterControlFactory
{
    /** @return AddToNewsletterControl */
    function create($componentSettings);
}
Oli
Člen | 1215
+
+7
-

Přijde mě, že to děláš zbytečně složitě. Taky jsem to dělal složitě, než jsem zjistil, že to jde i jednodušeji. Pokusím se nastínit, jak bych na to šel já.

  1. Vůbec bych nepředával nastavení komponenty presenteru. Presenteru je úplně jedno, jaký nastavení ta komponenta má a je to závislost, kterou vůbec nemusí mít. Pro předávání závislostí se podívej na tenhle článek: http://www.zeminem.cz/…nitive-guide
  2. Pokud nepotřebuješ nějaký speciální továrničky, tak bych to dělal tím interface a autowire, jak píše @jiri.pudil
  3. Pro každou komponentu bych předával vlastní nastavení. Je to lepší z hlediska znovupoužitelnosti. Pokud by jsi to měl rozdělené do nějaký ch bloků/modulů, tak nastavení pro celý modul. Mám to tak taky, kdy například articleModule je v podstatě soubor několika komponent a má jedno nastavení, protože se vždy použije jako celek.
  4. negeneroval bych všechny komponenty v basePresenteru. Jestliže jich máš 25 a budou přibývat, tak nepředpokládám, že je všechny potřebuješ na všech stránkách. Myslím, že pokud je nepoužiješ, tak se ani nevytvoří, ale stejně mi to nepřijde jako dobrý nápad.

Celé by to potom mohlo vypadat nějak takhle:

parameters:
	addToNewsletter:
	    testkey1: testvalue1
		testkey2: testvalue2
		# ...

services:
    - implement: AddToNewsletterControlFactory
      arguments: [%addToNewsletter%]
interface AddToNewsletterControlFactory
{
    /** @return AddToNewsletterControl */
    function create( /*$componentSettings*/ ); // EDIT: tenhle parametr není potřeba
}
class AddToNewsletterControl extends Nette\Application\UI\Control
{
	public function __construct($configParam, Model\User\UserRepository $userRepository, Model\Mailing\MailingNewsletterEmailRepository $mailingNewsletterEmailRepository) {}
}
public function createComponentAddToNewsletterControl($name, AddToNewsletterControl $addControl)
{
	return $addControl->create();
}

Editoval Oli (28. 4. 2015 10:20)

tolljump
Člen | 47
+
0
-

Z toho vytvarania komponent som stale nejak blazon…
Nemozem do configu zadavat do parameters nejake nastavenia, pretoze tie sa tahaju z DB a su naplnene v premennej $componentSettings.

Tym interface tiez stale trochu nerozumiem… Pretoze ja Factory pouzivam na injectovanie vsetkych repositarov (ako som ukazal v kode v prvom prispevku), no v interface to nieje mozne (alebo sa mylim?).

Ale ako vravis @Oli , tak nebude idealne ich vsetky vkladat do BasePresentra. Ako ich teda mat kedykolvek a kdekolvek dostupne?
Mam rozne komponenty ktore potrebujem mat dostupne napriec vsetkymi presentermi.

Rsp. co je pre mna este väcsi problem, potreboval by som vediet akukolvek komponentu pouzivat v akejkolvek inej komponente. Je to nejakym SPRAVNYM sposobom mozne? Nemyslim si ze predavat trait LoadFactories v ktorom mam vsetky komponenty definovane je spravne riesenie. Pride mi to uplne skarede a „neprogramatorske“.
V tom trait sa mi zacina hromadit mnozstvo kodu so stale sa opakujucim zapisom

// ADD TO NEWSLETTER
    private $addToNewsletterControlFactory;

    public function injectAddToNewsletterControlFactory(\App\Components\AddToNewsletterControlFactory $factory){
        $this->addToNewsletterControlFactory = $factory;
    }
    protected function createComponentAddToNewsletter(){
        $componentsSettings = $this->componentsSettings['addToNewsletter'];
        return $this->addToNewsletterControlFactory->create($componentsSettings);
    }

Tu by som sa vratil este k tomu $componentSettings. Ako som vravel, je to pole vytiahnute z databazy.
Cize tak ako si pisal ze kazdej komponente vyberat len jej nastavenia by znamenalo niekolko-nasobne viac DB selectov ktore su podla mna zbytocne.
Momentalne mam raz v BasePresentri vytvorenu public premennu $componentsSettings ktora je naplnena jednym DB dotazom.
Je mozne sa tejto premennej nejakym sposobom zbavit? Aby som mal napr. nejaky objekt/komponentu/cokolvek co by vykonalo raz select do DB a nasledne by som odtial tahal v komponentach uz len nastavenia tej danej komponenty?

newPOPE
Člen | 648
+
0
-

A co takto ist na to cez ComponentFactory ktora ti komponentu aku potrebujes vyrobi (btw: tak funguje /fungovala createComponent($name) v Presenteri).

ComponentFactory Ti vyrobi komponentu aku potrebujes. Takisto si ta ista factory moze loadnut konfiguracie z DB, cache … a riadit si ich ako potrebuje.

Na druhej strane to este mozes urobit aj tak ze proste pouzije mechanizmus aky Nette poskytuje a to je predavanie zavislosti do component automacky pomocou DI. A nastavenia si tam tiez mozes poslat z konfigu zo sluzby ktora bude obsahovat jednotlive konfiguracie pre komponenty nieco taketo:

services:
	componentConfiguration: ComponentConfiguration

	-
		class: MyControl(@componentConfiguration::getConfig('my'))
		implement: IMyFactory

Co sa tyka predania predania dalsich zavislosti do MyControl tak mam pocit ze DI container sa o to postara nieco ako:

class MyControl ... {
	function __construct($config, Foo $foo, Bar $bar) {
		# tu by mali byt automaticky predane Foo a Bar a $config si si nastavil v config.neon
	}
}
tolljump
Člen | 47
+
0
-

No to predavanie konfiguracie pre komponentu pomocou configu vyzera zaujimavo. Ale ako to bude fungovat v pripade vsetkych komponent ak pri kazdej budem mat v configu:

class: MyControl(@componentConfiguration::getConfig('my'))
class: FooControl(@componentConfiguration::getConfig('foo'))
class: BarControl(@componentConfiguration::getConfig('bar'))

Ako by vyzeral class componentConfiguration? Napr.:
__construct by volal metodu setComponentsConfig()
metoda setComponentsConfig() by spravila dotaz na databazu z ktorej by vybrala vsetky nastavenia komponent a nastavila ich do private $componentsConfig.
Nasledne metoda getConfig($name) by vyberala z $this->componentsConfig[$name] nastavenia danej komponenty.
Spravne?

Otazka: nebude sa takto dotazovat do databazy vzdy kazda komponenta ktorej sa bude predavat ako parameter tato trieda componentConfiguration?

newPOPE
Člen | 648
+
0
-

Odpoved: ked si to napises tak aby sa na DB viac ako 1× nesahalo tak sa nebude. Ked si to napises tak, ze si to componentConfiguration loadne z DB a ulozi do cache tak sa to moze tahat z cache. Tam uz zalezi co a ako potrebujes…