Nefungující DI ve vztahu Prezentér <⇒ Model

zachrdlapetr
Člen | 49
+
0
-

Trápím se už nějakou dobu s DI, které mi nevkládá kontext databáze. Prošel jsem diskuzní fóra, ale nikde jsem na tento problém nenarazil. poradíte mi prosím kde mám chybu?

vrací chybu na řádku 12 v modelu: Call to a member function table() on null
model

 namespace App\Models;
 4:    use Nette;
 5:    use Nette\Database\Context;
 6:
 7:    class Test  {
 8:        /** @var \Nette\Database\Context @inject */
 9:        public $database;
10:
11:        public function getTest() {
12:            return $this->database->table('test');
13:
14:        }
15:    }
prezentér se dotazuje do modelu a model se dotazuje do databáze

 5:    use Nette;
 6:    use App\Model;
 7:
 8:    class HomepagePresenter extends BasePresenter
 9:    {
10:        /** @var \App\Models\Test @inject */
11:        public $test;
12:
13:        public function renderDefault() {
14:            $this->template->test = $this->test->getTest();
15:        }
16:        public function createComponentTestovacitabulka() {
        		$test = new Test;
		        return $test->getTest();

		   }

Stejná chyba v modelu vyskakuje, když se dotážu z komponenty

namespace App\Controls;
use App\Models\Test;

class TestControl extends BaseControl {
    public function createComponentTestovacitabulka() {
        $test = new Test;
        return $test->getTest();

    }
}
ali
Člen | 342
+
+2
-

Injectni databazi v modelu pres constructor.

pavelmlejnek
Člen | 16
+
+3
-

Ta třída Test musí Database\Context získat nějak jinak, protože u služeb ve výchozím stavu anotace inject nefunguje (lze zapnout). Nejvhodější bude použít konstruktor, tedy:

namespace App\Models;

    use Nette;
    use Nette\Database\Context;

    class Test  {
        /** @var \Nette\Database\Context */
        public $database;

        public function __construct(\Nette\Database\Context $database)
		{
			$this->database = $database;
		}

        public function getTest() {
            return $this->database->table('test');
        }
    }

U komponent je situace o něco jiná (viz dokumentace), doporučuju použít továrničku .

zachrdlapetr
Člen | 49
+
0
-

pavelmlejnek napsal(a):

Ta třída Test musí Database\Context získat nějak jinak, protože u služeb ve výchozím stavu anotace inject nefunguje (lze zapnout). Nejvhodější bude použít konstruktor, tedy:

namespace App\Models;

    use Nette;
    use Nette\Database\Context;

    class Test  {
        /** @var \Nette\Database\Context */
        public $database;

        public function __construct(\Nette\Database\Context $database)
		{
			$this->database = $database;
		}

        public function getTest() {
            return $this->database->table('test');
        }
    }

U komponent je situace o něco jiná (viz dokumentace), doporučuju použít továrničku .

Děkuji za komentář. Celý den nad tím laboruji, ale nemám žádný pozitivní výsledek. Když volám model přímo z prezentéru, tak metoda __construct funguje zcela bezchybně, ale v okamžiku, kdy požaduji přístup z komponenty, tak model hází chybu, že jsem nevložil \Nette\Database\Context $database. Jenže já to pokaždé vyplňovat nechci. Lze to nějak elegantně obejít, aby v komponentě mohlo být jen níže uvedené? @inject zde opět nefunguje.

namespace App\Controls;
use App\Models\Test;

class TestControl extends BaseControl {


    public function createComponentTestovacitabulka() {
        $test = new Test;
        return $test->getTest();

    }



}
Ondřej Kubíček
Člen | 494
+
+3
-

musíš si ten model zaregistrovat v configu

services:
	- App\Models\Test

pak si ho, stejně jak databázi, získat přes contructor v tom TestControl, nevytvářet ho ručně

class TestControl extends BaseControl {
	public function __construct(App\Models\Test $testModel){...}
}
zachrdlapetr
Člen | 49
+
0
-

Děkuji všem za rady. Dumal jsem jak tu situaci vyřešit, protože můj problém spočíval v tom, že jsem nechápal předávání závislostí v nette.

Situace: Máme na straně logiky prezenční a modelovou vrstvu. Prezenční vrstva obsahuje i komponenty, které jsou rozšířením prezenční vrstvy, ale chovají se jako modelová vrstva. V prezenční vrstvě je možné DI používat ve formě @inject a inject*. V modelové vrstvě a v komponentech je možné použít a je doporučeno použít DI pomocí kontejnerů.

Prošel jsem několik aplikací, abych zjistil jak to dělají jiní a stejně jako tady kolegové všichni využívají princip, že na prezenční vrstvě DefaultPresenter vloží pomocí @inject modelovou třídu DefaultModel, kterou pak zdědí do DefaultControl a odtud volají metody v DefaultModel. Důvod je jasný, protože @inject nevytváří novou instanci a není problém s DI v konstruktorech modelové vrstvy, kde se obvykle nachází databáze, protože jinak by museli zdědit Context včetně Connection, čímž by vytvářeli novou instanci připojení do databáze.

To mě bohužel přijde jako škrabání levou rukou za pravým uchem. Takže jsem problém vyřešil jinak. Abych získal nezávislost komponent na tom, kam se budou dotazovat (do kterého modelu) a nebyly závislé na předávání všech modelů (všichni si definují přes @inject všechny modely, které budou potřebovat v komponentách), které jsou nutné k chodu komponenty, rozhodl jsem se předávat právě jen databázi z DefaultPrezenter. To mi umožní mít skutečně oddělené komponenty, modely a prezentéry. Spojovacím prvkem bude pouze předání databáze, které bude vždy stejné, takže nemusím dumat nad tím co budu všechno muset předávat. Níže je ukázka, kdybych všechno chtěl dělat jen přes komponenty. Co si o tom myslíte? Vidíte v tom něco špatného?

class HomepagePresenter extends \Nette\Application\UI\Presenter
{
    /** @var \Nette\Database\Context @inject */
    public $database;

    public function renderDefault() {


    }
    public function createComponentZobrazitTestControl() {
        return new \App\Controls\TestControl($this->database);

    }
}


class TestControl extends  \Nette\Application\UI\Control {
  public $database;

    public function __construct(\Nette\Database\Context $database) {
        parent::__construct();
        $this->database = $database;

    }


      public function render() {
            $this->template->setFile(__DIR__ . '/TestControl.latte');

            $testmodel = new TestModel($this->database);
            $this->template->test = $testmodel->getTest();
            $this->template->render();

    }

}

class TestModel extends \Nette\Object {

    /** @var $database */
    public $database;

    public function __construct(Context $database) {
        $this->database = $database;
    }

    public function getTest() {
        return $this->database->table('test')->fetchAll();
    }

}

Editoval zachrdlapetr (29. 1. 2018 10:27)

Ondřej Kubíček
Člen | 494
+
+2
-

a není lepší (spoiler je), když si vytvoříš abstract BaseModel který dostane v constructoru tu databázi, pak jen ta testModel extenduje od té abstraktní

ty komponenty bys měl vytvářet přes továrny viz. https://doc.nette.org/…tion/factory
a v té komponentě si předáš až ten konkrétní model, viz má ukázka výše

přes presenter si nebudeš předávat žádné závislosti, je to zbytečné, když si to ta komponenta dokáže získat sama

zachrdlapetr
Člen | 49
+
0
-

Ondřej Kubíček napsal(a):

a není lepší (spoiler je), když si vytvoříš abstract BaseModel který dostane v constructoru tu databázi, pak jen ta testModel extenduje od té abstraktní

ty komponenty bys měl vytvářet přes továrny viz. https://doc.nette.org/…tion/factory
a v té komponentě si předáš až ten konkrétní model, viz má ukázka výše

přes presenter si nebudeš předávat žádné závislosti, je to zbytečné, když si to ta komponenta dokáže získat sama

Db mám samozřejmě v BaseModel stejně jako BaseControl, ale pro zjednodušení jsem to upravil, aby bylo vidět vše.

Továrničky jsou pro mě stále záhadou. Zatím v nich vidím jen práci navíc. Jakou výhodu mi přinese použití továrničky ke komponentě, když musím ke každé komponentě naprogramovat továrničku a pak ji inejktovat do prezentéru a teprve pak použít v továrničce createComponent? V mém řešení přenesu jen DB do komponenty a ta už nic od prezentéru nepotřebuje.

David Matějka
Moderator | 6445
+
+2
-

Presenter by se nemel starat o to, jak komponentu vytvorit – zodpovednost presouvas na factory. Factory se obecny pattern, ktery se uplatnuje na ruznych mistech.
Prakticka vyhoda je napr. to, ze pokud bys pridal novou zavislost komponenty, tak se v presenteru nemusis o nic starat. Dale se mnohem snadneji predavaji neautowirovatelne parametry.

tovarnu „naprogramovat“ nemusis, jen vytvoris rozhrani a o implementaci se postara nette. navic, kdyz pouzijes tento plugin do phpstormu, tak ti vygeneruje to rozhrani

zachrdlapetr
Člen | 49
+
0
-

David Matějka napsal(a):

Presenter by se nemel starat o to, jak komponentu vytvorit – zodpovednost presouvas na factory. Factory se obecny pattern, ktery se uplatnuje na ruznych mistech.
Prakticka vyhoda je napr. to, ze pokud bys pridal novou zavislost komponenty, tak se v presenteru nemusis o nic starat. Dale se mnohem snadneji predavaji neautowirovatelne parametry.

tovarnu „naprogramovat“ nemusis, jen vytvoris rozhrani a o implementaci se postara nette. navic, kdyz pouzijes tento plugin do phpstormu, tak ti vygeneruje to rozhrani

Ale proč? Jaká je ta zjevná výhoda továrničky klidně přes interface, kterou nevidím?

Pro mě je to aktuálně jen toto v prezentéru (kód níže). O nic dalšího se nestarám. Je mi úplně jedno co se děje v komponentě. Když bude potřebovat komponenta se dotazovat do nových a nových modelů, tak je to její starost.

public function createComponentTestControl() {
    return new \App\Controls\TestControl($this->database);
}
David Matějka
Moderator | 6445
+
+2
-

Když bude potřebovat komponenta se dotazovat do nových a nových modelů, tak je to její starost.

ale budes ty zavislosti muset predat z presenteru. a o to jde – presunout zodpovednost o vytvoreni komponenty pryc z presenteru

zachrdlapetr
Člen | 49
+
0
-

David Matějka napsal(a):

Když bude potřebovat komponenta se dotazovat do nových a nových modelů, tak je to její starost.

ale budes ty zavislosti muset predat z presenteru. a o to jde – presunout zodpovednost o vytvoreni komponenty pryc z presenteru

teď předávám jedinou závislost a tou je databáze. další závislosti se předávat nebudou, protože komponenta si otevře nové instance modelových tříd. je to vidět v mém příkladu výše (prezentér, komponenta, model).

Co je na tomto špatně?

Resp. plyne z vytvoření nové instance komponenty v prezentéru nějaký problém v budoucnu?

Editoval zachrdlapetr (29. 1. 2018 11:39)

Šaman
Člen | 2666
+
+3
-

Je to úplně jiný přístup, než je DI. A není moc praktický.

DIC (dependency injection container) má být jediný objekt, který je zodpovědný za vytvoření všech služeb. Když se přejde na tuhle architekturu, tak ti z aplikace téměř zmizí všechny operátory new.

  1. Jednu třídu modelu může vyžadovat několik různých komponent. U tebe se bude pokaždé vyutvářet nová instance.
  2. Z veřejného rozhraní není jasné, co která komponenta vyžaduje. Když si vykopiruješ komponentu do jiného projektu, ve kterém neexistuje modelová třída, kterou i uvnitř skrytě instancuješ, tak to spadne. Navíc nemusí být potřeba vždy, takže něco rychle odzkoušíš a funguje to, ale jindy ti to začne vyhazovat chyby. Všechny nutné závislosti by si komponenta (obecně jakákoliv třída) měla vyžádat přes její rozhraní (konstruktor, settery).
  3. Nedá se to testovat. Není možné předat té tvé komponentě nějaký falešný model, který umí vrátit data pro testy. Pokud si ktřídy své závislosti vyžádají, tak je možné jen změnou configu spustit celou aplikaci s testovacími daty (tedy často s jinými třídami modelu).
Šaman
Člen | 2666
+
+1
-

To, co jsem psal výše platí pro vytváření tříd

zachrdlapetr napsal(a):
Resp. plyne z vytvoření nové instance komponenty v prezentéru nějaký problém v budoucnu?

Pokud tvoji komponentu bude vytvářet presenter, bude potřebovat všechny její závislosti. Nejsou to závislosti presenteru, tedy on sám je nijak nevyužívá.

Když si komponentu nechá vytvořit DI kontejnerem, tak dostane nakonfigurovanou instanci komponenty (nebo její továrničky). V metodě createComponent…() si pak pomocí té továrničky vytvoří unikátní instanci, která se rovnou zařadí do stromu komponent s vazbou na mateřský presenter. Ale presenteru je úplně jedno, jestli ta komponenta chce pět modelů, jednu databázi, třídu pro logování a navíc třeba nějaký specifický FormRenderer. To už není věc toho presenteru, nemusí ho to zajímat.

zachrdlapetr
Člen | 49
+
0
-

Šaman napsal(a):

Je to úplně jiný přístup, než je DI. A není moc praktický.

DIC (dependency injection container) má být jediný objekt, který je zodpovědný za vytvoření všech služeb. Když se přejde na tuhle architekturu, tak ti z aplikace téměř zmizí všechny operátory new.

  1. Jednu třídu modelu může vyžadovat několik různých komponent. U tebe se bude pokaždé vyutvářet nová instance.
  2. Z veřejného rozhraní není jasné, co která komponenta vyžaduje. Když si vykopiruješ komponentu do jiného projektu, ve kterém neexistuje modelová třída, kterou i uvnitř skrytě instancuješ, tak to spadne. Navíc nemusí být potřeba vždy, takže něco rychle odzkoušíš a funguje to, ale jindy ti to začne vyhazovat chyby. Všechny nutné závislosti by si komponenta (obecně jakákoliv třída) měla vyžádat přes její rozhraní (konstruktor, settery).
  3. Nedá se to testovat. Není možné předat té tvé komponentě nějaký falešný model, který umí vrátit data pro testy. Pokud si ktřídy své závislosti vyžádají, tak je možné jen změnou configu spustit celou aplikaci s testovacími daty (tedy často s jinými třídami modelu).
  1. ano budu vždy vytvářet novou instanci, ale to bych považoval spíš za výhodu, ne?
  2. ano tak to je, ale je to komponenta. je to balíček, který se o něco stará a využívá společnou modelovou vrstvu a na prezenční vrstvě mě to nezajímá, protože úkolem prezenční vrstvy je vložit komponentu do šablony.
  3. ano to nejde, ale předpokládám, že k tomu není důvod. v komponentách neočekávám žádné zpracování informací, které chci dávat pouze do modelů. myslím, že by v komponentech ani být logika neměla. nebo se mýlím?
zachrdlapetr
Člen | 49
+
0
-

Šaman napsal(a):

To, co jsem psal výše platí pro vytváření tříd

zachrdlapetr napsal(a):
Resp. plyne z vytvoření nové instance komponenty v prezentéru nějaký problém v budoucnu?

Pokud tvoji komponentu bude vytvářet presenter, bude potřebovat všechny její závislosti. Nejsou to závislosti presenteru, tedy on sám je nijak nevyužívá.

Když si komponentu nechá vytvořit DI kontejnerem, tak dostane nakonfigurovanou instanci komponenty (nebo její továrničky). V metodě createComponent…() si pak pomocí té továrničky vytvoří unikátní instanci, která se rovnou zařadí do stromu komponent s vazbou na mateřský presenter. Ale presenteru je úplně jedno, jestli ta komponenta chce pět modelů, jednu databázi, třídu pro logování a navíc třeba nějaký specifický FormRenderer. To už není věc toho presenteru, nemusí ho to zajímat.

To ale platí i pro ten můj způsob. V prezentéru vytvořím jen novou třídu komponenty new App\Controls\TextControl a další závislosti z prezentéru do komponenty neposílám pokud to není potřeba (např. vnější vstupy).

Resp. jinak. Nemáš prosím odkaz na nějaký fungující prototyp jak to myslíš, abych to mohl osahat? Možná mi to pomůže změnit názor. Protože mi zatím přesunutí new mimo prezentér připadne jen jako více práce. Abys rozuměl tomu proč rebeluji, tak nechci dělat nějaké kroky navíc jen aby to bylo z hlediska architektury zcela v pořádku, ale bez zjevného užitku a vlivu. Já jsem prostě zjednodušovatel :)

Šaman
Člen | 2666
+
+2
-

zachrdlapetr napsal(a):
To ale platí i pro ten můj způsob. V prezentéru vytvořím jen novou třídu komponenty new App\Controls\TextControl a další závislosti z prezentéru do komponenty neposílám pokud to není potřeba (např. vnější vstupy).

Viz ten můj první příspěvek. Neposíláš, ale vytváříš si její závislosti přímo v komponentě, což je ještě horší.

Ukázkou je každý novější Nette projekt, teorii proič je to lepší máš třeba tady a pokračování tady.

Editoval Šaman (29. 1. 2018 12:05)

zachrdlapetr
Člen | 49
+
0
-

Šaman napsal(a):

zachrdlapetr napsal(a):
To ale platí i pro ten můj způsob. V prezentéru vytvořím jen novou třídu komponenty new App\Controls\TextControl a další závislosti z prezentéru do komponenty neposílám pokud to není potřeba (např. vnější vstupy).

Viz ten můj první příspěvek. Neposíláš, ale vytváříš si její závislosti přímo v komponentě, což je ještě horší.

Ukázkou je každý novější Nette projekt, teorii proič je to lepší máš třeba tady a pokračování tady.

jj. tento článek znám, ale stále mi z toho není patrná ta výhoda.

To, že vytvářím v komponentě závislost na modelové třídě ti připadne v nepořádku?

Mě zase nepřipadne v pořádku mít komponentu, která se má chovat jako balíček, které musím říkat co má být v balíčku, přičemž v tom samém balíčku se stejně odkazuji do modelové třídy, kterou jsem předal z prezentéru, a která stejně musí odpovídat té samé třídě, do které se dotazuji nezávisle z komponenty.

Je to

class TestControl \Nette\Application\UI\Control {

    public $database;

    public function __construct(\Nette\Database\Context $database) {
        parent::__construct();
        $this->database = $database;

    }
    public function render() {

            $this->template->setFile(__DIR__ . '/TestControl.latte');

            $testmodel = new TestModel($this->database);
            $this->template->test = $testmodel->getTest();
            $this->template->render();

    }



}

vs.

class TestControl \Nette\Application\UI\Control {

    public $testmodel;

    public function __construct(App\Controls\TestModel $testmodel) {
        parent::__construct();
        $this->testmodel = $testmodel;

    }
    public function render() {

            $this->template->setFile(__DIR__ . '/TestControl.latte');

            $this->template->test = $this->testmodel->getTest();
            $this->template->render();

    }



}

S tím rozdílem, že do prezentéru musím model injectovat @inject a pak vložit do komponenty anebo na to udělat továrničku, kterou si pomohu odstranit instanci komponenty z prezentéru, ale závislost na TestModel musím tak jak tak předat z prezentéru do továrničky, která to předá do komponenty.

A já mám prostě stále pocit, že komponenta by měla sama vědět co má dělat a jaké k tomu potřebuje závislosti (v mém případě nová instance TestModel). Tím druhým způsobem v podstatě říkám komponentě jaké má zdroje k tomu co má dělat, ale to co má dělat je neoddělitelné od toho jakou dostane závislost. Takže klidně mohu do komponenty poslat špatnou závislost, ale to co má dělat se nemění.

Šaman
Člen | 2666
+
+1
-

To není o tom, že komponenta má závislost, ale že u tebe má skrytou závislost. Pracoval jsi někdy v týmu? Tam se takovéhle věci stávají peklem…

zachrdlapetr
Člen | 49
+
0
-

Šaman napsal(a):

To není o tom, že komponenta má závislost, ale že u tebe má skrytou závislost. Pracoval jsi někdy v týmu? Tam se takovéhle věci stávají peklem…

Ano závislost je skrytá v komponentě. Ale čemu to vadí? Jaké rizika to přináší do týmu, když komponenta má závislost až v samotné komponentě?

Šaman
Člen | 2666
+
+2
-

zachrdlapetr napsal(a):
S tím rozdílem, že do prezentéru musím model injectovat @inject a pak vložit do komponenty anebo na to udělat továrničku, kterou si pomohu odstranit instanci komponenty z prezentéru, ale závislost na TestModel musím tak jak tak předat z prezentéru do továrničky, která to předá do komponenty.

Nikoliv, pokud použiješ továrničku správně, tak presenter o žádném TestModelu nebude vůbec vědět. Nezajímá ho to. To je jen věc té komponenty.

zachrdlapetr napsal(a):
A já mám prostě stále pocit, že komponenta by měla sama vědět co má dělat a jaké k tomu potřebuje závislosti (v mém případě nová instance TestModel). Tím druhým způsobem v podstatě říkám komponentě jaké má zdroje k tomu co má dělat, ale to co má dělat je neoddělitelné od toho jakou dostane závislost. Takže klidně mohu do komponenty poslat špatnou závislost, ale to co má dělat se nemění.

  1. Nechceš novou instanci TestModel. Modelová třída by měla být služba a když deset komponent bude chtít TestModel, dostanou všechny právě tu jednu konkrétní instanci TestModelu.
  2. Ano, komponenta má vědět, co potřebuje. A má to říct hned na začátku (ideálně v konstruktoru). Ale nemá si to sama vytvářet. Komponenta ve složitějších projektech nemá dostatečné kompetence o vytváření instancí. Ta si o ně má říct.

Třeba proto, až tvůj TestModel bude po úpravách vyžadovat další závislost, tak o tom budeš muset říct každé komponentě, která si ho vytváří. A každou znovu odladit. Přitom do logiky těch komponent nic nepřibylo, jen je musíš všechny upravit (s rizikem nových chyb a překlepů). Když dostanou už nakonfigurovaný TestModel, tak jim je jedno, co se změnilo v implementaci TestModelu. Důležité je jen, aby zůstalo zachované jeho API.

Šaman
Člen | 2666
+
+1
-

zachrdlapetr napsal(a):

Ano závislost je skrytá v komponentě. Ale čemu to vadí? Jaké rizika to přináší do týmu, když komponenta má závislost až v samotné komponentě?

Ptáš se, co je špatného na tom, že nevím, na jakém dalším kódu je komponenta závislá? No, hlavně to, že nevím, na jakém dalším kódu je ta komponenta závislá… :)
Ta tvoje komponenta navenek neříká nic o tom, že chce nějaký TestModel. Mohu ji i vytvořit (předám jí jen databázi) a všechno se tváří v pohodě. Pak spustím aplikaci a vyběhne na mě hláška, že třída TestModel není k dispozici. WTF? Jaký TestModel? Já přece s žádným nepracuji…

Když si o něm řekne v konstruktoru, tak se najde chyba už při sestavování aplikace. A hlavně mám informaci o nutnosti TestModelu už když poprvé vezmu tu komponentu do ruky.

zachrdlapetr
Člen | 49
+
0
-

Šaman napsal(a):

zachrdlapetr napsal(a):
S tím rozdílem, že do prezentéru musím model injectovat @inject a pak vložit do komponenty anebo na to udělat továrničku, kterou si pomohu odstranit instanci komponenty z prezentéru, ale závislost na TestModel musím tak jak tak předat z prezentéru do továrničky, která to předá do komponenty.

Nikoliv, pokud použiješ továrničku správně, tak presenter o žádném TestModelu nebude vůbec vědět. Nezajímá ho to. To je jen věc té komponenty.

zachrdlapetr napsal(a):
A já mám prostě stále pocit, že komponenta by měla sama vědět co má dělat a jaké k tomu potřebuje závislosti (v mém případě nová instance TestModel). Tím druhým způsobem v podstatě říkám komponentě jaké má zdroje k tomu co má dělat, ale to co má dělat je neoddělitelné od toho jakou dostane závislost. Takže klidně mohu do komponenty poslat špatnou závislost, ale to co má dělat se nemění.

  1. Nechceš novou instanci TestModel. Modelová třída by měla být služba a když deset komponent bude chtít TestModel, dostanou všechny právě tu jednu konkrétní instanci TestModelu.
  2. Ano, komponenta má vědět, co potřebuje. A má to říct hned na začátku (ideálně v konstruktoru). Ale nemá si to sama vytvářet. Komponenta ve složitějších projektech nemá dostatečné kompetence o vytváření instancí. Ta si o ně má říct.

Třeba proto, až tvůj TestModel bude po úpravách vyžadovat další závislost, tak o tom budeš muset říct každé komponentě, která si ho vytváří. A každou znovu odladit. Přitom do logiky těch komponent nic nepřibylo, jen je musíš všechny upravit (s rizikem nových chyb a překlepů). Když dostanou už nakonfigurovaný TestModel, tak jim je jedno, co se změnilo v implementaci TestModelu. Důležité je jen, aby zůstalo zachované jeho API.

  1. ano to beru, můj důvod pro nové a nové instance je nefungující @inject v modelech a komponentách (asi je k tomu důvod)
  2. ano. platí to stejné o @inject v bodu 1

Předpokládal jsem, že modelová vrstva bude konečná a závislosti mezi modely bude předávat komponenta. Tedy, že nikdy nenastane situace, kdy by si model požádal napřímo o data z jiného modelu. Tím jsem chtěl udržet jeho nezávislost.

zachrdlapetr
Člen | 49
+
0
-

Šaman napsal(a):

zachrdlapetr napsal(a):

Ano závislost je skrytá v komponentě. Ale čemu to vadí? Jaké rizika to přináší do týmu, když komponenta má závislost až v samotné komponentě?

Ptáš se, co je špatného na tom, že nevím, na jakém dalším kódu je komponenta závislá? No, hlavně to, že nevím, na jakém dalším kódu je ta komponenta závislá… :)
Ta tvoje komponenta navenek neříká nic o tom, že chce nějaký TestModel. Mohu ji i vytvořit (předám jí jen databázi) a všechno se tváří v pohodě. Pak spustím aplikaci a vyběhne na mě hláška, že třída TestModel není k dispozici. WTF? Jaký TestModel? Já přece s žádným nepracuji…

Když si o něm řekne v konstruktoru, tak se najde chyba už při sestavování aplikace. A hlavně mám informaci o nutnosti TestModelu už když poprvé vezmu tu komponentu do ruky.

Ano komponenta to navenek neříká, že bude potřebovat TestModel. Ale já myslím, že o to ani nestojím, aby to prezentér věděl co dělá komponenta. Považuji komponentu za uzavřený celek, který vkládám se vším všudy do prezentéru. Tedy já jsem to chtěl záměrně oddělit. Ale samozřejmě nad tím váhám, protože jsem do teď měl jiný systém. Proto hledám „užitečný důvod“ proč to dělat přes továrny.

Editoval zachrdlapetr (29. 1. 2018 12:58)

David Matějka
Moderator | 6445
+
+2
-

Ale já myslím, že o to ani nestojím, aby to prezentér věděl co dělá komponenta.

no ale od toho je prave ta tovarna. uz ted vi, ze komponenta potrebuje databazi. co kdyz bude potrebovat komponenta navic mailer? jak ho tam dostanes? nebo bude potrebovat cache

zachrdlapetr
Člen | 49
+
0
-

David Matějka napsal(a):

Ale já myslím, že o to ani nestojím, aby to prezentér věděl co dělá komponenta.

no ale od toho je prave ta tovarna. uz ted vi, ze komponenta potrebuje databazi. co kdyz bude potrebovat komponenta navic mailer? jak ho tam dostanes? nebo bude potrebovat cache

udělal bych to takto.

class TestControl \Nette\Application\UI\Control {

    public $database;

    public function __construct(\Nette\Database\Context $database) {
        parent::__construct();
        $this->database = $database;

    }
    public function render() {

            $this->template->setFile(__DIR__ . '/TestControl.latte');

            $testmodel = new TestModel($this->database);
            $this->template->test = $testmodel->getTest();

			$mail = new \Nette\Mail\Message;
			$mail->setFrom('email@email')
			    ->addTo('email@email')
			    ->setSubject('Data')
			    ->setBody("posílám data.");

			$mailer = new \Nette\Mail\SendmailMailer;
			$mailer->send($mail);


            $this->template->render();



    }



}
David Matějka
Moderator | 6445
+
+2
-

to je spatne, komponenta nema mit zodpovednost (a nema ani tu znalost jak a ktery) mailer ma vytvorit. co kdyz budes potrebovat smtp mailer? nebo uplne jinou implementaci?

zachrdlapetr
Člen | 49
+
0
-

David Matějka napsal(a):

to je spatne, komponenta nema mit zodpovednost (a nema ani tu znalost jak a ktery) mailer ma vytvorit. co kdyz budes potrebovat smtp mailer? nebo uplne jinou implementaci?

když to bude nettí

public function render()
	$mailer = new Nette\Mail\SmtpMailer([
	    'host' => 'smtp.gmail.com',
	    'username' => 'franta@gmail.com',
    	'password' => '*****',
	    'secure' => 'ssl',
	]);
	$mailer->send($mail);
}

když vlastní

public function render()
	$mailer = new MailModel;
	$mailer->send();
}
David Matějka
Moderator | 6445
+
+2
-

a jak komponenta zjisti, ktery ma pouzit? odkud vezme ty udaje?

co kdyz budes mailer pouzivat ve vice komponentach?

procti si par clanku o DI. zakladni princip je v tom, ze namas zavislosti vytvaret, ale rict si o ne.

zachrdlapetr
Člen | 49
+
0
-

Stále váhám. Fakt nechci dělat nic navíc. Továrnička a předávání všech instancí modelů do komponenty konstruktem stále zatím vnímám jen jako trendy prvek. Narazil prosím někdo na zcela konkrétní situaci, kdy moje schéma je vyloženě blbost? Pomineme-li testování, které jsem říkal, že je otázka pro modelovou třídu v mém případě.

zachrdlapetr
Člen | 49
+
0
-

David Matějka napsal(a):

a jak komponenta zjisti, ktery ma pouzit? odkud vezme ty udaje?

co kdyz budes mailer pouzivat ve vice komponentach?

procti si par clanku o DI. zakladni princip je v tom, ze namas zavislosti vytvaret, ale rict si o ne.

pokud bude mailer ve více komponentách, tak nová instance v komponentě. Komponentu beru jako funkční celek. Proto do ní nechci předávat informaci, kteoru by sama měla znát.

Články o DI jsem si přečetl, ale i po přečtení to stále vnímám jen jako způsob práce. Rád bych v tom viděl trochu víc, a to pro mě znamená mít důvody, kdy můj způsob práce se zcela bez pochyby ukáže jako nefunkční.

David Matějka
Moderator | 6445
+
+2
-

kdyz nechces delat nic navic, tak je DI presne pro tebe :) nebudes se muset v komponentach starat o to, jak vytvorit jednotlive modelove tridy, jak ziskat cache, jak nakonfigurovat mailer…

zachrdlapetr
Člen | 49
+
0
-

David Matějka napsal(a):

kdyz nechces delat nic navic, tak je DI presne pro tebe :) nebudes se muset v komponentach starat o to, jak vytvorit jednotlive modelove tridy, jak ziskat cache, jak nakonfigurovat mailer…

Máš prosím nějaký nějaký ultimátní příklad, který můj způsob práce roznese? Abych neměl vůbec žádnou pochybnost, že předávání všeho přes DI je pro mě jediná cesta.

Pavel Kravčík
Člen | 1196
+
0
-

@zachrdlapetr: Ono některé zkušenosti jsou asi nepřenositelné. Až budeš například dělat aplikaci rok ve 4 lidech a následně jí za 2 roky například upravovat – velmi doceníš přístup Davida či Šamana.

Konkrétní situace: do mail modelu budeš potřebovat přidat přílohu (generované PDF ze tří tabulek). Příloha se generuje ze 2 modelů a každý ten model má 4 závislosti. A z těch 4 závislostí mají další 2 závislosti – takže tam v podstatě začneš duplikovat DI kontejner a ještě špatně.

David Matějka
Moderator | 6445
+
+3
-

jako ten mailer mi prijde celkem jasny priklad. kdyz ho vyzadas pres DI, tak nemusis vubec v komponente resit, co je to za implementaci a jak je nakonfigurovana. na produkci bude v nejakym local.neon nakonfigurovany treba mailer pres api mandrillu, jeden vyvojar si v konfigu pro vyvoj nastavi smtp na mailtrap, nekdo jinej treba bude preferovat nextras mail panel. ale komponente to bude zcela jedno – bude ji predana implementace maileru. a pak je ten druhy krok, kdy presenteru je jedno, co ma komponenta za zavislosti – a v tu chvili vstupuji do hry tovarny, ktere umeji vytvaret tu komponentu. ve vysledku pouze nakonfigurovany DI kontejner zna presne postupy, jak zkontruovat vsechny sluzby, komponenty, presentery a co kam predat.

zeptam se te trochu jinak, proc ses rozhodl, ze database si budes predavat a mailer vytvaret?

CZechBoY
Člen | 3608
+
+2
-

Ultimátní případ není potřeba, je jen potřeba dostat pod kůži jednoduchost DI a přijmout ji.
Jak bys řešil např. situaci kdy chceš poslat mail, ale nevíš přes co ten mail poslat (v metodě, která mail vytvoří – vloží adresáty, text emailu, …). Totiž ty můžeš u jedný instalace aplikace chtít posílat maily přes SMTP, v další budeš používat AWS, jinde obyč mail() metodu. Nebo taky můžeš používat SMTP všude – ale zase budeš mít jiné přihlašovací údaje :-).
Teď si představ co to znamená udělat na jednom místě (v tom tvém ručně vypsaném příkladu)… no a teď neposíláš maily jen z jednoho místa ale třeba z 20. Takže na 20 místech bys měl new SmtpMailer(natvrdo údaje).

A teď to porovnej s DI:
konfigurace:
viz třeba dokumentace https://doc.nette.org/cs/configuring#…
aplikace:

class NotifyOverEmail
{
	private $mailer;

	public function __construct(\Nette\Mail\IMailer $mailer)
	{
		$this->mailer = $mailer;
	}

	public function notify()
	{
		$message = ...;

		$this->mailer->send($message); // to je vše!
	}
}

Jo ještě abych odpověděl rozdíl mezi @inject a konstruktorem. Inject anotace @inject a inject metody public function injectAbc(zavislost) jsou frameworkem povolené pouze v Presenter třídách. Ano, můžeš si inject povolit všude, ale je to prasárna a já inject nepoužívám jinde než v abstraktních předcích (Base* třídách). Inject je jen featura frameworku, která ti ulehčí předávání závislostí. Rozdíl oproti konstruktoru tam prakticky není – služby se vždy vytváří pouze jednou (jedna instance).

zachrdlapetr
Člen | 49
+
0
-

Pavel Kravčík napsal(a):

@zachrdlapetr: Ono některé zkušenosti jsou asi nepřenositelné. Až budeš například dělat aplikaci rok ve 4 lidech a následně jí za 2 roky například upravovat – velmi doceníš přístup Davida či Šamana.

Konkrétní situace: do mail modelu budeš potřebovat přidat přílohu (generované PDF ze tří tabulek). Příloha se generuje ze 2 modelů a každý ten model má 4 závislosti. A z těch 4 závislostí mají další 2 závislosti – takže tam v podstatě začneš duplikovat DI kontejner a ještě špatně.

No děláme aplikace ve více než 4 lidech, ale na vlastní platformě, která DI závislostí nevyužívá. Proto neznám záludnosti, výhody.

Ke konkrétní situaci jsem už popisoval, že žádný model nemá závislosti mezi sebou. Chtěl jsem aby modelová vrstva neměla žádnou závislost a aby se informace z modelů procesovaly v jiném modelu, ale informace mezi nimi propojí komponenta. Tedy ne vždy nutně předá závislost, ale spíš, že předá informace k dalšímu zpracování.

Rob Bob
Člen | 60
+
+1
-

Vždyť už jsi tu dostal několik konkrétních příkladů s konkrétními výhodami.

zachrdlapetr
Člen | 49
+
0
-

David Matějka napsal(a):

jako ten mailer mi prijde celkem jasny priklad. kdyz ho vyzadas pres DI, tak nemusis vubec v komponente resit, co je to za implementaci a jak je nakonfigurovana. na produkci bude v nejakym local.neon nakonfigurovany treba mailer pres api mandrillu, jeden vyvojar si v konfigu pro vyvoj nastavi smtp na mailtrap, nekdo jinej treba bude preferovat nextras mail panel. ale komponente to bude zcela jedno – bude ji predana implementace maileru. a pak je ten druhy krok, kdy presenteru je jedno, co ma komponenta za zavislosti – a v tu chvili vstupuji do hry tovarny, ktere umeji vytvaret tu komponentu. ve vysledku pouze nakonfigurovany DI kontejner zna presne postupy, jak zkontruovat vsechny sluzby, komponenty, presentery a co kam predat.

zeptam se te trochu jinak, proc ses rozhodl, ze database si budes predavat a mailer vytvaret?

Mailer: ano ten je nakonfigurován v neon. s tím, že každý programátor si nastaví jiný mailer moc nesouhlasím. beru smtpmailer a sendmailmailer. ale také neznám v tuto chvíli omezení. navíc pokud si každý programátor přidá svůj mailer (hypoteticky), tak stejně jej musím vždy ten konkrétní vložit do komponenty přes konstrukt, což je naprosto ten stejný úkon jako udělat novou instanci.

Databaze: nepředával bych ani databázi kdyby komponenty a modely uměly @inject :) Ale jelikož chci mít model nezávislý, tak potřebuji v něm minimálně databázi. Ostatní už není pro mě o předávání závislostí, ale pouze informací mezi třídami (řekli jsme, že nechci závislost mezi modelovými třídami). Znamená to tedy, že jsem chtěl, aby model byl dostupný i z prezentéru i z komponenty a jediná možnost jak tam dostat databázi, pokud nechci sestavovat celé Connection až v BaseModel, je ho injectovat konstruktorem.

David Matějka
Moderator | 6445
+
+1
-

tak stejně jej musím vždy ten konkrétní vložit do komponenty přes konstrukt, což je naprosto ten stejný úkon jako udělat novou instanci.

to vubec neni pravda, v konstruktoru vyzadas IMailer a je ti jedno, jakou implementaci ti DI kontejner preda

CZechBoY
Člen | 3608
+
+1
-

@zachrdlapetr jo, v konstruktoru ho klidně nastavuj, ale jde o to kdo vytvoří instanci toho modelu (instanci třídy).
Představ si třeba, že komponenta má 3 závislosti a každá závislost má třeba 2 závislosti – to je 6 závislostí namísto 3. A ještě když změníš nějaký konstruktor služby tak bys musel na x místech změnit vytváření té třídy.
Proč prostě nechceš nechat vytváření té třídy na někom, kdo má k dispozici instance všech tříd v aplikaci?

U toho maileru jsem to trošku překombinoval a už se nebavíme o DI třídy ale rovnou rozhraní – to jsem zašel asi moc daleko co se týká výhod DI…

Editoval CZechBoY (29. 1. 2018 14:23)

Šaman
Člen | 2666
+
+1
-

zachrdlapetr napsal(a):

Články o DI jsem si přečetl, ale i po přečtení to stále vnímám jen jako způsob práce. Rád bych v tom viděl trochu víc, a to pro mě znamená mít důvody, kdy můj způsob práce se zcela bez pochyby ukáže jako nefunkční.

Máš prosím nějaký nějaký ultimátní příklad, který můj způsob práce roznese? Abych neměl vůbec žádnou pochybnost, že předávání všeho přes DI je pro mě jediná cesta.

Jasně, že to není jediná cesta. Je to jen způsob práce.
Ale je to cesta, která se ukázala jako funkční i v případech, že komplexita projektu začne narážet na limity mnoha jiných řešení.

Základem jsou pravidla „každá třída má dělat jen jednu věc“ (Single responsibility principle) a „jedna věc by se měla řešit jen na jednom místě“ (Don’t Repeat Yourself).
Tvůj přístup porušuje obě. Komponenta kromě své logiky řeší i vytváření dalších objektů. A vytváření těch objektů je rozdrobeno do mnoha komponent. A dokonce ani neexistuje soupis všech míst, kde třeba takový mailer (jak píšeš výše) vytváříš. Pokud budeš chtít udělat jedinou změnu, znamená to projít a změnit až desítky tříd. Zachraňuje tě jen fulltextové vyhledávání…

Tohle už nesouvisí přímo s Nette, ale obecně s OOP programováním. Nette ve skutečnosti nic takového nevyžaduje a předávej, či vytvářej si to jak chceš. Jen ti s tím pak málokdo bude ochotný radit, protože většina Nette programátorů a aplikací využívá postupů, které u tebe nebudou.

Ještě přidám jeden volně související odkaz: Návrhové principy: SOLID

Editoval Šaman (29. 1. 2018 14:26)

zachrdlapetr
Člen | 49
+
0
-

CZechBoY napsal(a):

Ultimátní případ není potřeba, je jen potřeba dostat pod kůži jednoduchost DI a přijmout ji.
Jak bys řešil např. situaci kdy chceš poslat mail, ale nevíš přes co ten mail poslat (v metodě, která mail vytvoří – vloží adresáty, text emailu, …). Totiž ty můžeš u jedný instalace aplikace chtít posílat maily přes SMTP, v další budeš používat AWS, jinde obyč mail() metodu. Nebo taky můžeš používat SMTP všude – ale zase budeš mít jiné přihlašovací údaje :-).
Teď si představ co to znamená udělat na jednom místě (v tom tvém ručně vypsaném příkladu)… no a teď neposíláš maily jen z jednoho místa ale třeba z 20. Takže na 20 místech bys měl new SmtpMailer(natvrdo údaje).

A teď to porovnej s DI:
konfigurace:
viz třeba dokumentace https://doc.nette.org/cs/configuring#…
aplikace:

class NotifyOverEmail
{
	private $mailer;

	public function __construct(\Nette\Mail\IMailer $mailer)
	{
		$this->mailer = $mailer;
	}

	public function notify()
	{
		$message = ...;

		$this->mailer->send($message); // to je vše!
	}
}

Jo ještě abych odpověděl rozdíl mezi @inject a konstruktorem. Inject anotace @inject a inject metody public function injectAbc(zavislost) jsou frameworkem povolené pouze v Presenter třídách. Ano, můžeš si inject povolit všude, ale je to prasárna a já inject nepoužívám jinde než v abstraktních předcích (Base* třídách). Inject je jen featura frameworku, která ti ulehčí předávání závislostí. Rozdíl oproti konstruktoru tam prakticky není – služby se vždy vytváří pouze jednou (jedna instance).

Dostat DI pod kůži: :) když děláš 14 let v jiném systému, tak to jde ztuha :)

Instlace: u všech instalací je to stejné. máme vlastní servery (Německo, Francie, Holandsko – různé výhody poskytovatelů :) nikdy to nejde na cizí server s výjimkou intranetu. když to jde na intranet, tak vždy na námi nakonfigurovaný lamp nebo xamp. Proto mě ani možnost, že bychom dělali mailer jinak vůbec nenapadla.

Příklad: vidíš to a já bych na toto vůbec žádný nový model nedělal. Prostě bych to odpálil z komponenty. Je to komponenta.

Inject DI: znám a teď už vím, že jen prezentéry a také je mám už jen v BasePresenter.

zachrdlapetr
Člen | 49
+
0
-

Rob Bob napsal(a):

Vždyť už jsi tu dostal několik konkrétních příkladů s konkrétními výhodami.

Dostal. Ale asi mi to zatím nedocvaklo, nebo to nepovažuji zatím za výhodu.

Rob Bob
Člen | 60
+
+1
-

@zachrdlapetr využíval jsi někdy při programování rozhraní (Interface)?

zachrdlapetr
Člen | 49
+
0
-

David Matějka napsal(a):

tak stejně jej musím vždy ten konkrétní vložit do komponenty přes konstrukt, což je naprosto ten stejný úkon jako udělat novou instanci.

to vubec neni pravda, v konstruktoru vyzadas IMailer a je ti jedno, jakou implementaci ti DI kontejner preda

to ale platí kdyby naprosto všechny služby a rozšíření používaly stejný interface. přesto však když komponentu volám, tak ji musím nakrmit závislostí a to je pro mě stále ten krok navíc. Oproti vytváření instance modelové třídy v komponentě. já tam vidím v tuto chvíli jen jedinou výhodu na příkladu interface IMailer, že mohu injektovat různé mailery. Ale jinak je to pro mě práce navíc. Rád bych to přijal, ale zatím se to ve mě pere.

zachrdlapetr
Člen | 49
+
0
-

CZechBoY napsal(a):

@zachrdlapetr jo, v konstruktoru ho klidně nastavuj, ale jde o to kdo vytvoří instanci toho modelu (instanci třídy).
Představ si třeba, že komponenta má 3 závislosti a každá závislost má třeba 2 závislosti – to je 6 závislostí namísto 3. A ještě když změníš nějaký konstruktor služby tak bys musel na x místech změnit vytváření té třídy.
Proč prostě nechceš nechat vytváření té třídy na někom, kdo má k dispozici instance všech tříd v aplikaci?

U toho maileru jsem to trošku překombinoval a už se nebavíme o DI třídy ale rovnou rozhraní – to jsem zašel asi moc daleko co se týká výhod DI…

Závislosti: do komponenty vkládám jen modely. ty jsem uvažoval, že mezi sebou nebudou mít závislosti, jen si budou mezi sebou předávat informace ke zpracování.

Rob Bob
Člen | 60
+
+2
-

Tak ještě znova – představ si situaci, kdy budeš chtít komponentu využít na více místech (znovupoužitelnost je důležitá vlastnost komponent), ty na více místech v kódu budeš mít tedy něco jako:

<?php
public function createComponentZobrazitTestControl() {
   return new \App\Controls\TestControl($this->database);
}
?>

V budoucnu třeba přijde požadavek rozšířit funkčnost komponenty o logování – pak bys musel nějakou třídu Logger předat nejprve do všech presenterů (který ho navíc ani sám nevyžaduje), kde chceš používat komponentu, a navíc všude přepsat kód z return new \App\Controls\TestControl($this->database) na return new \App\Controls\TestControl($this->database, $this->logger) místo toho abys to upravil na jednom místě – v továrně pro TestControl. V presenterech už bys měl totiž všude jenom univerzální return $this->testControlFactory->create(). Presenter nepotřebuje znát jaké třídy komponenta využívá.

Mysli dopředu, že bude někdy potřeba aplikovaci upravovat, škálovat atd., což ti korektní používání DI (a dalších OOP návrhových vzorů, nevymýšlej kolo) do budoucna usnadní. To stojí za to minimum práce navíc, když ty třídy píšeš poprvé.

David Matějka
Moderator | 6445
+
+2
-

přesto však když komponentu volám, tak ji musím nakrmit závislostí a to je pro mě stále ten krok navíc.

nemusis, o to se prave stara ta tovarna

CZechBoY
Člen | 3608
+
+1
-

Hele jak může být práce navíc psaní méně věcí? :-))

  1. komponenta vytváří všechny závislosti
class MyComponent extends Control
{
	private $database;
	private $mailer;
	private $templateFactory;

	public function __construct(Database $database, IMailer $mailer, ITemplateFactory $templateFactory)
	{...}

	public function ???()
	{
		$recipientsModel = new RecipientsModel($this->database);
		$emailModel = new SendEmailModel($this->database, $this->mailer, $this->templateFactory); // chceme přece hezčí HTML šablonu, tak ji vygenerujeme přes ITemplateFactory

		$recipients = $recipientsModel->getRecipients();
		$sendEmailModel->send($recipients);
	}
}
  1. komponenta dostane 2 modely přes DI
class MyComponent extends Control
{
	private $recipientsModel;
	private $sendEmailModel;

	public function __construct(RecipientsModel $recipientsModel, SendEmailModel $sendEmailModel)
	{...}

	public function ???()
	{
		$recipients = $this->recipientsModle->getRecipients();
		$this->sendEmailModel->send($recipients);
	}
}
  1. komponenta dostane fasádu
class MyComponent extends Control
{
	private $sendEmailFacade;

	public function __construct(SendEmailFacade $sendEmailFacade)
	{...}

	public function ???()
	{
		$this->sendEmailFacade->send();
	}
}

class SendEmailFacade
{
	private $sendEmailModel;
	private $recipientsModel;

	public function __construct(SendEmailModel $sendEmailModel, RecipientsModel $recipientsModel)
	{...}

	public function send()
	{
		$recipients = $this->recipientsModel->getRecipients();
		$this->sendEmailModel->send($recipients);
	}
}

ps. varianta C je více upsaná, ale pokud bys to používal na více místech tak ti budoucí změny ušetří hodně práce.