Komponenty teoreticky, vytvářet komponentu pro položku, výpis položek, apod.?

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

Ahoj, měl bych spíše diskusi k zamyšlení, jestli jdu správným směrem. V diskusi jsem četl, že bych mohl celý web poskládat pomocí komponent a protože už to tak dělám v CSS i JavaScriptu, rád bych i v PHP.

Mám nějaký výpis položek, ty budou prokliknutelné do detailu. Výpis může být různých podob, řádkový, blokový apod.

Je tedy správná cesta vytvořit komponentu Položka (která by měla signály například přidat do oblíbených, komentovat, zobrazit komentáře) a pak další komponentu Výpis položek (která by měla například signál pro načtení dalších položek)?

Díky za navedení ;-)

Azathoth
Člen | 495
+
0
-

určitě to je správná cesta. protože se ti pak veškerá logika přesune z presenterů pryč a komponenty budeš moci pohodlně používat ve více presenterech.

joe
Člen | 313
+
0
-

Díky, určitě s tím souhlasím, ale trochu jsem se zasekl.

Bez komponent mám šabloně pro výpis něco takového:

{foreach $cards as $card}
	<div class="Card">
    	<a n:href="Card:detail $card->id">
        	<img src="{$card->image|thumb:240,320,true}">
        	<span class="Card-info">{$card->name}</span>
    	</a>
	</div>
{/foreach}

Vytvořil jsem si tedy komponentu Card, kam jsem přesunul celý HTML kód s <div class="Card">,
tím mi v kódu vznikne

{foreach $cards as $card}
	{control card}
{/foreach}

Ale (pravděpodobně?) potřebuji v komponentě Card mít instanci $card ještě dříve, než před renderováním (proto nemám v šabloně {control card, $card}). Chtěl bych mít v komponentě signály třeba jako handleAddToFavorite. Nevím jak tam tu instanci správně předat.

Dál mám v komponentě odkaz na Card:detail, což se mi také nezdá ideální, protože tím komponentě říkám, jaká je struktura presenterů a to by ji přece nemělo zajímat… (?)

Prosím o nakopnutí, díky :-)

greeny
Člen | 405
+
0
-

Multiplier

protected function createComponentCard()
{
	return new Multiplier(function ($name) {
		$control = new CardControl; // nebo cardControlFactory->create();
		$control->setCard($name);
		return $control;
	});
}
{foreach $cards as $card}
	{control card-$card}
{/foreach}

Editoval greeny (22. 10. 2015 15:19)

joe
Člen | 313
+
0
-

Multiplier se mi bude hodit, ale v šabloně při vytváření komponenty se používá její název a já bych té komponentě potřeboval už předat instanci, protože v $card mám entitu karty, mohu předat jen její id, ale to si ji pak budu muset přes entity manager znovu vytáhnout, i když ji už vlastně jednou na jiném místě mám nebo ne?

Takže mi možná nezbývá než si vytvořit pole komponent už v presenteru, předat je šabloně, kde je jen vypíšu (?)

Editoval joe (22. 10. 2015 14:28)

Azathoth
Člen | 495
+
0
-

to je v pořádku, že je budeš muset přes entity manager znovu vytáhnout. Protože pokud u entityManageru používáš metodu find, tak on se nejdřív podívá, jestli už není vytažená a pokud je, tak ti ji vrátí bez položení SQL dotazu.

Oli
Člen | 1215
+
0
-

@Azathoth on ale nepíše, že používá Doctrine ;-)

@joe proč celý tý komponentě nepředáš pole entit/Selection/cokoli používáš? Máš k tomu nějaký důvod? Podle mě o hodně lepší než použít multiplier.

David Matějka
Moderator | 6445
+
0
-

A pokud nepouzivas doctrine, uloz si cards, ktere posilas do sablony, do nejake clenske promenne – kouknes se, jestli zaznam je tam a kdyz ne, vyberes z db.

joe
Člen | 313
+
0
-

Díky, momentálně na projektech mám LeanMapper.

@Azathoth Díky, možná to tak bude i u LeanMapperu, čekal bych to, ale nevím proč hledat něco znovu, když to v tu danou chvíli už mám k dispozici.

@Oli Tak to už by nebyla komponenta Card, ale třeba CardList, ne? :-) To jsem se právě chtěl dozvědět, co by bylo vhodnější… Refaktoruju si projekt a právě nevím jakou cestou se vydat, ale víc logické mi přijde – karta = 1 komponenta (?)

joe
Člen | 313
+
0
-

@DavidMatějka Takže je vhodnější vytvořit jednu komponentu, která se stará o výpis položek než komponentu pro každou položku?

Jiří Nápravník
Člen | 710
+
+1
-

V tomhle konkretním případě mi dává smysl vytvořit komponentu, která si vytáhne z databáze i vypíše všechny ty karty najednou. Proč mít {foreach}{control}{/foreach} když mužes mít jen jeden kontrol v šabloně? Jedině pokud bys tu komponentu využíval osamoceně pro vykreslení jedné karty.

link na Card:detail – asi není úplně konkrétní ale sám to používám, je to v podstatě odkaz někam jinam. Například mám anketu a chci mít odkaz an všechny ankety pod ní, tak nějaký link tam mít musím…

Zax
Člen | 370
+
0
-

Někdy stačí komponenta která obstarává celý seznam včetně položek, jindy se víc hodí mít zvlášť komponentu na seznam a v ní multiplierem komponenty-jednotlivé položky. Záleží dost na situaci a třeba mě osobně přijde většinou lepší to mít rozdělené. Ale pro jednoduché případy může v pohodě stačit jedna komponenta, proč ne, jenom se to nesmí moc rozrůstat ;-)

@joe Zmínil jsi například že tam chceš mít signály a to je podle mě už taková ta hranice kde bych přemejšlel nad rozdělením. Pokud jdu cestou jedné komponenty, tak si musím vždy v šabloně předat idčko do odkazu a v handle metodě pak zase převádět ičko zpátky na entitu (data), což může být nepříjemné. Pokud to mám rozdělené, nemusím nic řešit, entitu najdu v privátní proměnné (rodič ji musí předat, pochopitelně). Záleží asi kolik těch signálů bude, co je tolerovatelné na údržbu, ale prostě mi to smrdí copy&paste „programováním“.

A pozor taky na další subkomponenty. Kdybys měl třeba přidávání do oblíbených jako komponentu a šel cestou jedné komponenty, tak musíš použít multiplier na přidávání do oblíbených. Pak přidáš lajkování – a zas řešíš multiplier, pro každou subkomponentu znovu a znovu to samé (a když na to dojde, tak se jede jenom přes copy&paste). Ale pokud to máš rozdělené, tak řešíš multiplier jen na jednom místě a v komponentě pro položky už nic neřešíš a používáš komponenty pro lajkování atd normálně bez multiplieru a bezbolestně ;-)

EDIT: a ještě jedna věc – AJAX. Vždy mi přišlo jednodušší v signálu položky zavolat prostě $this->redrawControl() a v šabloně položky mít obyčejný snippet, než se snažit pochopit jak fungujou dynamický snippety.

Editoval Zax (22. 10. 2015 18:11)

joe
Člen | 313
+
0
-

@JiříNápravník Ten foreach bych sice duplikoval, ale myslel jsem, že bych pomocí té komponenty vypisoval kartu položky {controm item:card}, pak například dětail položky {control item:detail} (teď jsem to pojmenoval jinak, než výš) a měl bych všechny šablony a zobrazení v jedné komponentě.

Co se týká odkazu a rozšíření, chtěl bych si udělat seznam různých jednoduchých komponent, které vezmu z jednoho projektu a dám je do druhého a nebudu je muset upravovat a nijak do nich zasahovat, případně updatovat, tak, aby byly zpětně kompatibilní.

@Zax Musím přiznat, že se mi líbí víc ta možnost s více komponentama, už i kvůli tomu, co popisuješ, jenom mi pořád není jasné, jak do té komponenty zastřešující jednu položku předám entitu

entitu najdu v privátní proměnné (rodič ji musí předat, pochopitelně)

Poradil bys mi prosím, jak toho docílím? Mám tedy v cyklu v presenteru vytvářet komponenty a do šablony si předat jen pole vytvořených komponent? Předpokládám, že mi nebude stačit {control card-N}, protože potřebuju předat tu entitu z kódu a ještě před fází renderování.

Zax
Člen | 370
+
0
-

@joe Třeba si vyrob v seznamu metodu getData, která ti data nejdřív naloaduje do privátního pole (s idčky jako klíči) a pak už jen tahá z něj. V položce zase vyrobíš metodu setData, kterou zavoláš v createComponent a předáš si tu jednu položku kterou získáš přes getData()[$id]. Není to složité ;-)

Jiří Nápravník
Člen | 710
+
0
-

@joe to s tim control item:detail apod. jsem zkousel drive a byl tam problem s ajaxem, bralo to jen ten defaultni render a ne renderDetail() apod. ale mozna uz je to vyresene nejak…

Rozumim, tak pak tu mas moznost, kteoru taky nekdy vyuzivam, ze si dam setter pro ten odkaz do komponenty (pres closure), a poslu si ho tam pak z presenteru. ale neni to moc pekny, ale nic lepsiho me nenapadlo:)

joe
Člen | 313
+
0
-

@Zax Chvíli si s tím hraju a snažím se to pochopit, ale nemůžu docílit nějakého výsledku. Resp. pořád nevím, jak mám té komponentě pro položku předat tu entitu :)

Ve skutečnosti to bude ještě složitější, přibyde mi k tomu komponenta filtr, kterou jen položkám přidám.
Teď se mi nedaří dostat do Item entitu, viz // @TODO Jak sem dostanu entitu?

Našel jsem na fóru Předání parametrů do komponenty při použití Interface ale s tím jsem si nějak neporadil.

Přepsal jsem to do kódu, aby bylo vidět jak to všechno teď mám. Předem všem díky za rady, začínám se v tom opět po letech trochu orientovat…

<?php

namespace FrontModule;


/* ******************** Presenter Items ******************** */


class ItemsPresenter extends BasePresenter
{
    /** @var IItemListFactory @inject */
    public $itemListFactory;


    protected function createComponentItemList()
    {
        $control = $this->itemListFactory->create();
        return $control;
    }
}


/* ******************** Component Item List ******************** */


interface IItemListFactory {

    /** @return ItemList */
    public function create();
}

class ItemList extends \Nette\Application\UI\Control
{
    protected $templateFile = __DIR__ . '/ItemList.default.latte';

    /** @var IItemFactory */
    private $itemFactory;

    public function __construct(IItemFactory $itemFactory)
    {
        parent::__construct();
        $this->itemFactory = $itemFactory;
    }

    public function render()
    {
        $this->template->setFile($this->templateFile);

        $controls = [];
        $controls[] = $this->getComponent('item-1');
        $controls[] = $this->getComponent('item-2');
        $controls[] = $this->getComponent('item-3');

        $this->template->controls = $controls;

        $this->template->render();
    }


    protected function createComponentItem()
    {
        return new \Nette\Application\UI\Multiplier(function() {
            $control = $this->itemFactory->create();
            return $control;
        });
    }
}


/* ******************** Component Item ******************** */


interface IItemFactory {

    /** @return Item */
    public function create();
}

class Item extends \Nette\Application\UI\Control
{
    protected $templateFile = __DIR__ . '/Item.default.latte';

    private $entity; // @TODO Jak sem dostanu entitu?

    public function setEntity($entity)
    {
        $this->entity = $entity;
    }

    public function handleLike()
    {
        dump($this->entity);
    }

    public function render()
    {
        $this->template->setFile($this->templateFile);
        $this->template->render();
    }
}

Šablona pro ItemList.default.latte

<div class="ItemList">
    {foreach $controls as $control}
        {control $control}
    {/foreach}
</div>

<style>
    .ItemList {
        background: lightblue;
        overflow: hidden;
        padding: 5px;
    }

    .Item {
        background: lightcoral;
        padding: 20px;
    }

    .ItemList .Item {
        margin: 5px;
        float: left;
    }
</style>

Šablona Item.default.latte

<div class="Item">
    Item
    <a n:href="like!">Like</a>
</div>

Pak ještě v konfiguraci mám registrované továrny na komponenty

- FrontModule\IItemListFactory
- FrontModule\IItemFactory
vitkutny
Člen | 73
+
+2
-

@joe v ItemList budeš mít kolekci entit, kterou buď příjmeš přes konstruktor, nebo se o získání bude starat sama komponenta z repozitáře, to je na tobě

v šabloně Itemlist si kolekci proiteruješ a vykreslíš zanořenou komponentu Item v multiplieru pomocí unikátního klíče, na straně PHP, podle klíče vytáhneš entitu z repozitáře (případně z pole entit, pokud je známé z konstruktoru, nebo nejpozději v attached, ale o tohle se stará identity mapa, nebál bych se tedy vždy použít získání podle id přímo z repozitáře) a předáš jí komponentě Item přes továrničku přímo do konstruktoru, kde si ji uložíš do privátní proměnné

ItemList::render

$this->template->items = $this->repository->findAll();

ItemList.default.latte

<div class="ItemList">
    {foreach $items as $item}
        {control item-$item->id}
    {/foreach}
</div>

ItemList::createComponentItem

return new \Nette\Application\UI\Multiplier(function($id) {
            return $this->itemFactory->create($this->repository->getById($id));
        });

IItemFactory

interface IItemFactory {

    /**
	 * @param Entity $entity
     * @return Item
	 */
    public function create(Entity $entity);
}
class Item extends \Nette\Application\UI\Control
{

    private $entity; // @TODO Jak sem dostanu entitu?

	public function __construct(Entity $entity){
		$this->entity = $entity;
	}

    public function handleLike()
    {
        dump($this->entity);
    }

}
joe
Člen | 313
+
0
-

@vitkutny Díky, moc pomohlo. Jen škoda, že LeanMapper mi při vybrání kolekce a pak následném tahání entity podle ID posílá dotaz do DB, ale je možné, že je problém u mě.

Ještě jednou díky všem.

vitkutny
Člen | 73
+
0
-

@joe
v tom případě si pole entit indexované podle id připrav v attached (nebo pokud se o to stará presenter, předej v konstruktoru) a při vytvoření komponenty Item vytahuj přímo z tama

	private $items = [];

	protected function attached($presenter){
		parent::attached($presenter);
		$this->items = $this->repository->findAll();
	}

	public function render(){
		$this->template->items = $this->items;
	}

	protected function createComponentItem(){
		return new \Nette\Application\UI\Multiplier(function($id) {
    return $this->itemFactory->create($this->items[$id]);
});
	}
Pavel Macháň
Člen | 282
+
0
-

joe napsal(a):

@vitkutny Díky, moc pomohlo. Jen škoda, že LeanMapper mi při vybrání kolekce a pak následném tahání entity podle ID posílá dotaz do DB, ale je možné, že je problém u mě.

Ještě jednou díky všem.

LeanMapper to tahat znova bude. Máš možnost udělat toto:

EDIT: byl sem předběhnut :) @vitkutny

class MyPresenter extends \Nette\Application\UI\Presenter {

	/** @var Item[] */
	private $items = [];

	public function actionMyAction(){
		$this->items = $this->repository->getByMyAction();
	}

    public function renderMyAction(){
        $this->template->items = $this->items;
    }

	public function createComponentMyComponent(){
		return new \Nette\Application\UI\Multiplier(function($id) {
			if(!isset($items[$id])) {
				throw new MyException('Uknown item ID: '. $id);
			}
            return $this->itemFactory->create($items[$id]);
        });
	}
}

Editoval Pavel Macháň (23. 10. 2015 13:29)

joe
Člen | 313
+
0
-

@vitkutny To udělám, jen musím zkontrolovat, jestli můžu globálně změnit vracející se pole z repozitáře tak, že klíče budou id :)

Pavel Macháň
Člen | 282
+
0
-

joe napsal(a):

@vitkutny To udělám, jen musím zkontrolovat, jestli můžu globálně změnit vracející se pole z repozitáře tak, že klíče budou id :)

Pokud ti to nepůjde tak si je přeházej nebo si vytvoř find funkci, která to vtom listu najde.

Editoval Pavel Macháň (23. 10. 2015 13:32)

Zax
Člen | 370
+
+2
-

@vitkutny Já to většinou řeším getterem který volám místo sahání do property:

class List extends Control {

	private $items;

	private function getItems() {
		if($this->items === NULL) {
			$this->items = $this->model->findByXyz();
		}
		return $this->items;
	}

	function render() {
		$this->template->items = $this->getItems();
	}

}

Přijde mi to lepší z toho důvodu, že se data načtou jen v momentě kdy jsou potřeba. Můžeš třeba volat signál, který jen provádí redirect někam pryč – v ten moment vůbec nepotřebuješ nic načítat.

Někdy se může hodit i cachování na úrovni šablon – model se zavolá pouze poprvé a pak už se jen servíruje čisté html z cache. Stačí malá úprava:

class List extends Control {

	function render() {
		// místo zavolání předám callback
		$this->template->items = function() {
			return $this->getItems();
		};
	}

}
{cache}
{foreach $items() as $id => $item} {* nezapomenout závorky; až tady se volá getItems() *}
	{control item-$id}
{/foreach}
{/cache}
Jan Folwarczny
Člen | 10
+
0
-

joe napsal(a):

@vitkutny To udělám, jen musím zkontrolovat, jestli můžu globálně změnit vracející se pole z repozitáře tak, že klíče budou id :)

LeanMapper má defaultně ID jako klíče pole :)
Viz https://github.com/…pository.php#L298

Editoval Jan Folwarczny (23. 10. 2015 18:48)

joe
Člen | 313
+
0
-

@JanFolwarczny Tam to je :), teď debuguju rychle, tak snad dám správný řádek, přes který mi to jde a který tam ID jako klíč nedává:

https://github.com/…r/Entity.php#L771

a pak na řádku 823 to samé.

Šaman
Člen | 2666
+
0
-

Nechci tě odrazovat od LM, který momentálně sám používám, ale myslím, že už ani @Tharos ho nepoužívá a neudržuje. (?) Zkušenosti s tímto ORM má jen několik lidí a když narazíš na nějaký vážnější problém, tak pochybuji, že ti někdo fundovaně poradí. LM má stále hodně úzkou vazbu na databázi, takže i pro to kompletní rozkomponentování bych já už doporučil spíš Doctrine. Jeden větší projekt v LM teď dokončuji a právě nedokonalá abstrakce mi občas přidělává vrásky. Tharos ji ale uvedl mezi klady LM, takže to je opravdu vlastnost, nikoliv nevýhoda (LM je díky tomu o řády jednodušší než Doctrina). Na to, o co se pokoušíš, bych ale raději volil ORM s vysokou abstrakcí.

joe
Člen | 313
+
0
-

@Šaman Díky za tip, LM mi dost vyhovuje, s Doctrine ho porovnat nemůžu, protože jsem ji ještě nezkoušel a z časových důvodů a ne velkého zájmu o programování na serveru se do toho zřejmě pouštět ani nebudu :) A myslím, že k dokonalému rozkompomentování to stejně nedotáhnu, spíš tak, aby bylo jednoduché se pro mě v tom vyznat a na to mi LM zatím krásně vystačuje :)