Dynamické vyžiadanie služby

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

Ahojte,

Tvorím jednoduchú komponentu, ktorá na základe zadaného reťazca (klasický search input) prehľadáva moduly systému a z každého modulu mi vráti nejaku šablonu dát.

Kedže každý zákazník dostáva inú konfiguráciu modulov, chcem vytvoriť search, ktorý by požiadal iba nainštalované moduly o vrátenie nejakého výsledku.

Nemôžem si teda dopredu definovať služby (premenné triedy), lebo logicky nemusia existovať (ani fyzicky)…

Je nejako možné za behu požiadať DI container o získanie služby?

<?php
// tu by som nejakú triedu požiadal o získanie mailerservice registrovanej v cfg
$mailerService = "MailerModule\Services\MailerService";
$data = $mailerService->getMailsByQuery($this->getQuery());
?>
iNviNho
Člen | 352
+
0
-

Tak sa mi to celkom rýchlo podarilo vyriešiť.

Bude v pohode, ak to budem používať takto?

<?php
// container je injectnuty Nette\DI\Container
$this->container->getService("mailer.mailerService")->getAllByQuery($query);
?>
CZechBoY
Člen | 3608
+
0
-

No předáš si celej dic. Nebo si uděláš třídu, která ti vrátí všechny ty tvoje třídy, který splňujou nějakou podmínku (a stejně je bude hledat v dic).

CZechBoY
Člen | 3608
+
0
-

Ta služba bude registrovaná jen jednou DIC? Pokud jo tak ji můžeš normálně přes autowire předat do konstruktoru (nebo kamkoliv jinam).

GEpic
Člen | 566
+
0
-

iNviNho napsal(a):

Ahojte,

Tvorím jednoduchú komponentu, ktorá na základe zadaného reťazca (klasický search input) prehľadáva moduly systému a z každého modulu mi vráti nejaku šablonu dát.

Kedže každý zákazník dostáva inú konfiguráciu modulov, chcem vytvoriť search, ktorý by požiadal iba nainštalované moduly o vrátenie nejakého výsledku.

Nemôžem si teda dopredu definovať služby (premenné triedy), lebo logicky nemusia existovať (ani fyzicky)…

Je nejako možné za behu požiadať DI container o získanie služby?

<?php
// tu by som nejakú triedu požiadal o získanie mailerservice registrovanej v cfg
$mailerService = "MailerModule\Services\MailerService";
$data = $mailerService->getMailsByQuery($this->getQuery());
?>

Dokonce si můžeš povolit injectování v komponentě. :)

iNviNho
Člen | 352
+
0
-

@GEpic to viem, ale to by moj problem neriesilo :)

GEpic
Člen | 566
+
+1
-

iNviNho napsal(a):

@GEpic to viem, ale to by moj problem neriesilo :)

Aha, sorry. My toto řešíme configem. Prostě v configu si nadefinujeme dostupné komponenty k danému modulu s jejich továrničkou, no a podle toho jaké kdo má moduly, tak se mu taky složí seznam dostupných komponent.

Neon je super, můžeš jich mít kolik chceš, modulový, projektový a můžeš si pomocí nich nastavit každý projekt či modul přesně tak jak potřebuješ, anižby si kvůli projektu musel zasahovat do modulů.

Editoval GEpic (28. 9. 2016 18:46)

artemevsin
Člen | 61
+
+1
-

jak píše @CZechBoY, viděl bych to na vytvoření nějaké třídy/služby. něco jako:

<?php
class SearchService {
    /**
     * @var ModuleOne
     */
    private $moduleOne;
    /**
     * @var ModuleTwo
     */
    private $moduleTwo;

    /**
     * SearchService constructor.
     * @param ModuleOne $moduleOne
     * @param ModuleTwo $moduleTwo
     */
    public function __construct(ModuleOne $moduleOne, ModuleTwo $moduleTwo)
    {
        // autowiring komponent
        $this->moduleOne = $moduleOne;
        $this->moduleTwo = $moduleTwo;
    }

    /**
     * @param string $searchPhrase
     * @param \Nette\Security\User $user
     * @return array
     */
    public function search($searchPhrase, \Nette\Security\User $user)
    {
        // klidně to může být nějaká služba, která ti vrátí dostupné moduly
        $modules = $user->getIdentity()->getAvailableModules();

        $results = [];
        foreach ($modules as $moduleName) {
            $results += $this->getModule($moduleName)->search($searchPhrase);
        }

        return $results;
    }

    /**
     * @param string $name
     * @return IModule
     */
    private function getModule($name)
    {
        $map = [
            'moduleOne' => $this->moduleOne,
            'moduleTwo' => $this->moduleTwo
        ];

        if (!array_key_exists($name, $map)) {
            throw new \InvalidArgumentException;
        }

        return $map[$name];
    }
}
?>
Oli
Člen | 1215
+
+1
-

@artemevsin tohle je v podstatě malej container ;-)

Záleží na tom, kolik těch služeb může být. Pokud jedna a vždy jiná, tak bych si injectnul interface, zakázal autowire na nich a injectul si tu, kterou potřebuju. Nevýhoda je, že tyhle služby budeš muset předávat i jinam ručně. Takže, pokud se používají často, tak je to nevhodný.

Druhou možností by bylo prohledat interfaces. Něco jako používá Kdyby/doctrine. Pokud máš každý modul jako rozšíření, tak by jsi to mohl udělat v podstatě úplně stejně.

Pokud ne, tak službám, který chceš takhle injectovat nastavíš interface, řekněme třeba ISearchModule. A pak v extension projedeš všechny třídy, který implementujou tu interface a injectneš si je do toho searchFormu (Možná na to bude mít nette DI nějakou metodu, která to umí vrátit hezčeji než takhle blbě přes reflexi). Nedělej to nikde jinde nebo si to cachuj. V extension třídě se ti to nacachuje samo…

iNviNho
Člen | 352
+
0
-

Dá sa aj tak, zatiaľ som to vyriešil tak, že akonáhle mám modul v konštante SEARCH_MODULES tak to znamená, že som jej vytvoril službu aj na vyhľadávanie a priamo DI container mi ju vráti a môžem ju požiadať o vrátenie už nastavenej, ready šablony. V skratke takto:

<?php
	/**
     * Performs search in certain module
     * and renders out given result
     * @param string $resourceName
     */
    private function performSearch($resourceName) {

        $template = $this->getTemplate();

        if (in_array($resourceName, self::SEARCH_MODULES)) {
            $moduleSearch = $this->container->getService($resourceName.".".$resourceName."ModuleSearch");
            $moduleSearch->setup($this->getQuery());
            $template = $moduleSearch->getTemplate();
            $template->render();
        }
    }
?>
Oli
Člen | 1215
+
+1
-

To mě přijde jako dost naivní přístup :) Mimojiné to předpokládá přesný název služby. Předáváš celý container (to už mě přijde lepší to řešení @artemevsin). A zároveň musíš udržovat ve kterých modulech se to prohledává v konstantě. Jinými slovy: Za 2 roky, až na tom něco změníš (např. název služby), tak ti to přestane fungovat a ty na to budeš koukat jak tele na nový vrata. Vím o čem mluvím :)

Jako, jestli máš napnutý rozpočet, tak asi good enough, ale je to technologickej dluh. Pokud si to můžeš dovolit, tak bych rozhodně doporučil udělat co modul, to extension a pak už je strašně jednoduchý to navěšení toho interface na extension a projítí jako jsem psal výš. ;-)

iNviNho
Člen | 352
+
0
-

@Oli Samozrejme, nie je to finálne a netvrdím, že najlepšie a vlastne celkovo cítim code smell v tom, že niečo musí mať presný názov :-\

Každý modul registrujem ako extension: ArticleExtension, MailerExtension, InvoiceExtension atd atd atd…
Takisto v extensionoch definujem aj cesty pre slovníky pre Kdyby\Translation, takže viem o čom hovoríš.

To čo vlastne popisuješ v treťom odstavci sa mi páči, celkom sa s tým návrhom stotožňujem, čiže:

1. Vytvorím si napr.:

<?php
	- class ContactModuleSearch implements IModuleSearch ()
	- class InvoiceModuleSearch implements IModuleSearch ()
	- class MailerModuleSearch implements IModuleSearch ()
?>

2. V BaseModule, ktorý je rodičom všetkých modulov, mám vytvorenú triedu, ktorá jednoducho povedané získa z týchto searchov template z výsledkami vyhľadávania.

<?php
class GeneralSearch extends Control {

    /** @var ResourceService */
    private $resourceService;

    /** @var User */
    private $user;

    /** @var Container */
    private $container;

    private $query;

    /** List of modules that have search */
    const SEARCH_MODULES = ["mailer", "contact", "invoice"];

    public function __construct($query, ResourceService $resourceService, User $user, Container $container) {
        $this->query = $query;
        $this->resourceService = $resourceService;
        $this->user = $user;
        $this->container = $container;
    }

    /**
     * Checks allowed modules for current user
     * and performs search in them
     * For each module it renders its search result
     */
    public function render() {

        $activeResources = $this->resourceService->getAllActive();

        foreach ($activeResources as $activeResource) {
            if ($this->user->isAllowed($activeResource->getName(), "view")) {
                $this->performSearch($activeResource->getName());
            }
        }
    }

    /**
     * Performs search in certain module
     * and renders out given result
     * @param string $resourceName
     */
    private function performSearch($resourceName) {

        $template = $this->getTemplate();

        if (in_array($resourceName, self::SEARCH_MODULES)) {
            $moduleSearch = $this->container->getService($resourceName.".".$resourceName."ModuleSearch");
            $moduleSearch->setup($this->getQuery());
            $template = $moduleSearch->getTemplate();
            $template->render();
        }
    }

    private function getQuery() {
        return $this->query;
    }

}
?>

Aby sa zachovala štruktúra a zmenil som obsah funckie performSearch, tak potrebujem v nej pracovať s triedami, ktoré majú implementované rozhranie IModuleSearch a nad nimi zavolať metodu, ktorá vráti template na základe nejakého $query stringu.

Otázkou teda je ako do tohto controlu dostať(injectnúť triedy, ktoré implementujú dané rozhranie) a ako sa s ním dá následne pracovať… Skúsim pohľadať, ale idem vybehnúť na obed :)

Zatiaľ ďakujem za nakopnutie a nápady

Editoval iNviNho (29. 9. 2016 12:07)

newPOPE
Člen | 648
+
0
-

@iNviNho co tak hladat na zaklade tagu. Kde vlastne tie moduly mas registrovane? V nejakom tvojom systeme/registri alebo v DI containeri?

GEpic
Člen | 566
+
0
-

Za mě, pokud musíš udělat něco takového:

const SEARCH_MODULES = ["mailer", "contact", "invoice"];

Pak moduly a veškerá abstrakce postrádá smysl. Navíc když už to registruješ jako extension, pak už to nějaký zápis v configu má, proč se toho nedržet?

Editoval GEpic (29. 9. 2016 12:36)

iNviNho
Člen | 352
+
0
-

@GEpic zo zaciatku nemal kazdy modul pripraveny search a preto sa este checkovalo ci v tejto konstante je dany modul…

inak by zhavaroval riadok kde taham sluzbu z di containera :(

@newPOPE v hlavnom configu v extensions si regujem moduly :)

CZechBoY
Člen | 3608
+
+1
-

@iNviNho tak si vytáhni službu s tím, že ji nepotřebuješ getByType($type, $need=false) a pak zkontroluj jestli ti teda DIC něco vrátil nebo ne ($service===null).

Editoval CZechBoY (29. 9. 2016 14:00)

Oli
Člen | 1215
+
+1
-

Trochu jsem si s tím pohrál a samotnýho by mě zajímalo, jestli to jde i jednodušeji? Na tagy, jsem zapomněl, díky @newPOPE . Takže funkční by mělo být toto:

  1. To co už máš. Služby, který implementujou nějakou jednu interface
  2. Všem třídám přidáš nějaký tag, např. moduleSearch
  3. Třída, do které chceme nahrát všechny služby definovaný v kroku jedna by mohla vypadat nějak takhle (zkráceně)
class GeneralSearch extends Control {

	private $searchModules = [];

    public function __construct($query, ResourceService $resourceService, User $user) {
        $this->query = $query;
        $this->resourceService = $resourceService;
        $this->user = $user;
    }

	public function addModule(IModuleSearch $module) {
		$this->searchModules[] = $module;
    }

    private function performSearch($resourceName) {

        $template = $this->getTemplate();

        foreach($searchModules as $module) {
            $module->setup($this->query);
            $template = $module->getTemplate();
            $template->render();
        }
    }
}
  1. a konečně místo, kde injectneme ty moduly
public function beforeCompile() {
    parent::beforeCompile();

    $builder = $this->getContainerBuilder();

    $generalSearch = $builder->getDefinition($this->prefix('generalSearch'));
    foreach ($builder->findByTag('moduleSearch') as $name => $bool)
    {
        $statement = new Statement($builder->getDefinition($name)->getClass());
        $generalSearch->addSetup('addModule', [$statement]);
    }
}

Editoval Oli (29. 9. 2016 14:28)

iNviNho
Člen | 352
+
0
-

@CZechBoY ďakujem za nakopnutie
@Oli Super super super
Ďakujem vážim si to aj váš čas.

Celkovo môže byť problém aj v tom, že nepoznám až tak dobre silu DI a čo všetko pomocou nej dokážeme. Zajtra to v robote skúsim a sám som zvedavý ako to celé funguje a čo mi vráti napr. new Statement()

btw nemôže nastať ešte nejaky problém v tom, že triedu GeneralSearch vytváram cez IGeneralSearch, kedže query jej musím vždy dodať ručne?

<?php
/**
 * ....
 */
interface IGeneralSearch {

    /**
     * @return GeneralSearch
     */
    function create($query);

}
?>
Oli
Člen | 1215
+
+1
-

Pokud je IGeneralSearch jen továrnička na GeneralSearch, tak to vadit nebude v ničem.