Komponenta v modelu, nebo service?
- Freestyler
- Člen | 50
Ahoj,
mám vytvořený selectBox který mi vypisuje kategorie článků z DB.
Momentálně mám dotazy do DB v modelu a komponentu tvořím
v HomepagePresenteru. Teď jsem ale zjistil, že ji potřebuji použít
i třeba při přidání nového článku (ArticlePresenter).
Napadlo mě přesunout celou komponentu do modelu a pak ji jenom vykreslit v šabloně, ale jelikož s nette začínám, tak nevím jestli je to správný postup.
Zde co mám hotovo a funguje v HomepagePresenteru
HomepagePresenter
<?php
namespace AdminModule;
use Nette\Application\UI\Form;
/**
* Homepage presenter.
*/
class HomepagePresenter extends BasePresenter
{
/**
* @var \App\Model\Articles
* @inject
*/
public $articles;
/**
* Fetch all articles to table
*/
public function beforeRender() {
$this->template->articles = $this->articles->fetchAll();
}
/**
*
* Getting name of category from db and render in selectBox
*/
public function createComponentArticleMenu() {
$form = new Form();
$form->addSelect('category','Kategorie', $this->articles->getArticleCategories()->fetchPairs('id','name'))
->setPrompt('Všechny články');
$form->addSubmit('submit', 'Zobrazit');
$form->onSuccess[] = $this->articleMenuSuceeded;
return $form;
}
/**
*
* Redirect to same page and list articles with specific categoryID
*/
public function articleMenuSuceeded($form) {
$this->redirect('this', ['categoryID' => $form->values->category]);
}
public function renderDefault($categoryID) {
$this->template->articles = !empty($categoryID) ?
$this->articles->getByCategoryId($categoryID) :
$this->articles->fetchAll();
}
}
Model Articles
<?php
namespace App\Model;
/**
* Description of Articles
*
* @author Freestyler
*/
class Articles extends Base {
/*
* Get all articles from db (limited to 10)
*/
public function fetchAll() {
return $this->database->table('articles')
->limit('10');
}
/**
* Add article to db
*/
public function articleAdd() {
}
/**
*
* Get all categories
*/
public function getArticleCategories() {
return $this->database->table('categories');
}
/*
* Get specific article based on category_id
*/
public function getByCategoryId($categoryID) {
return $this->database->table('articles')
->where('categories_id', $categoryID);
}
}
Šablona HomepagePresenteru
{block content}
<br /> <br />
<div class="main"><h3>Posledních 10 příspěvků:</h3> <br />
{control articleMenu}
<br /> <br />
<table class="table-article-create">
<thead>
<tr>
<th>ID</th>
<th>Název</th>
<th>Text</th>
<th>Autor</th>
<th>Datum</th>
<th>Kategorie</th>
</tr>
</thead>
<tbody>
<tr n:foreach = "$articles as $article">
<td>{$article->id}</td>
<td>{$article->title}</td>
<td>{$article->content}</td>
<td>{$article->author}</td>
<td>{$article->datetime}</td>
<td>{($category = $article->ref('categories','categories_id')) ? $category->name : ('')}</td>
</tr>
</tbody>
</table>
</div>
{/block}
Nakopne mě prosím někdo, jak na to jít správně abych mohl ke komponentě přistupovat odkudkoliv? Nepotřebuji ji měnit, jen zobrazit a pak už z ní jen tahat data a ukládat do DB.
Díky moc.
- Tomáš Votruba
- Moderator | 1114
Ahoj, jestli to dobře chápu, potřebuješ zobrazit stejnout komponentu na více presenterech.
Pokud ano, stačí ji přesunout do BasePresenteru.
Dalším krokem by mohlo být vyčlenění komponenty do samostatné třídy (názorný příklad), abys měl BasePresenter čistý.
- Freestyler
- Člen | 50
Super, to mě vůbec nenapadlo, že to bude tak jednoduchý :).
Ještě bych měl jeden problém. V komponentě mám jeden submit, kterej potřebuju používat v HomePresenteru, ale při přidání článku (ArticlePresenter) bych potřeboval zobrazit jen ten selectBox bez tlačítka (tam mi stačí když si pak vytáhnu ID kategorie a uložím do DB).
Je možný to nějak udělat a nebo musím mít dvě prakticky identický komponenty, ale jednu s tlačítkem a druhou bez.
Díky.
EDIT: jediný co mě napadlo, tak to tlačítko schovat pomocí CSS, ale to asi nebude moc správně…
Tomáš Votruba napsal(a):
Ahoj, jestli to dobře chápu, potřebuješ zobrazit stejnout komponentu na více presenterech.
Pokud ano, stačí ji přesunout do BasePresenteru.
Dalším krokem by mohlo být vyčlenění komponenty do samostatné třídy (názorný příklad), abys měl BasePresenter čistý.
Editoval Freestyler (6. 7. 2014 13:10)
- Tomáš Votruba
- Moderator | 1114
Na to krásně hodí mít komponentu v samostatné třídě.
Pak bys mohl udělat něco jako:
HomepagePresenter.php
protected function createComponentArticleMenu()
{
return $this->articleMenuFactory->create();
}
ArticlePresenter.php
protected function createComponentArticleMenu()
{
$control = $this->articleMenuFactory->create();
$control->useSubmit(FALSE);
return $control;
}
Jak na to se dočteš v pěkném návodu
Editoval Tomáš Votruba (6. 7. 2014 13:39)
- Freestyler
- Člen | 50
Díky, vypadá to moc pěkně, jen mi to nefunguje :(. Pročetl jsem jak best practise:formuláře jako komponenty, tak návod, který jsi postoval ty a dostávám:
Method AdminModule\HomepagePresenter::createComponentArticleMenu() did not return or create the desired component
Mám to takhle:
ComponentFactory
<?php
namespace App\Model;
use Nette\Application\UI\Form;
class ComponentFactory {
protected function createComponentArticleMenu() {
$form = new Form();
$form->addSubmit('send', 'Odeslat');
return $form;
}
}
interface IArticleMenuFactory {
/** @return ComponentFactory */
function create();
}
HomepagePresenter
<?php
namespace AdminModule;
/**
* Homepage presenter.
*/
class HomepagePresenter extends BasePresenter
{
/** @var \App\Model\IArticleMenuFactory
* @inject
*/
public $componentFactory;
protected function createComponentArticleMenu() {
return $this->componentFactory->create();
}
}
V configu mám zaregistrován interface:
services:
- App\Model\IArticleMenuFactory
V šabloně pak klasicky používám {control articleMenu}
Tomáš Votruba napsal(a):
Na to krásně hodí mít komponentu v samostatné třídě.
Pak bys mohl udělat něco jako:
HomepagePresenter.php
protected function createComponentArticleMenu() { return $this->articleMenuFactory->create(); }
ArticlePresenter.php
protected function createComponentArticleMenu() { $control = $this->articleMenuFactory->create(); $control->useSubmit(FALSE); return $control; }
Jak na to se dočteš v pěkném návodu
- David Matějka
- Moderator | 6445
ComponentFactory
musi byt komponenta, takze musi dedit
Nette\Application\UI\Control a musi mit render metodu. (+ to pojmenuj
rozumeji)
- Freestyler
- Člen | 50
Jéje, tak to mě nenapadlo. A tam musím tu šablonu ručně vykreslit, jak je popsáno tady https://doc.nette.org/…n/components ? Nebo to jde nějak obejít :).
- David Matějka
- Moderator | 6445
Pokud ma komponenta pouze vykreslovat svoji podkompoentu, ktera je formularem, tak bude stacit:
public function render()
{
$this['articleMenu']->render();
}
- Freestyler
- Člen | 50
Paráda, díky. Škoda, že to v tom návodu není napsáno, začátečníky to může asi dost zmást.
matej21 napsal(a):
Pokud ma komponenta pouze vykreslovat svoji podkompoentu, ktera je formularem, tak bude stacit:
public function render() { $this['articleMenu']->render(); }
- Šaman
- Člen | 2666
Freestyler napsal(a):
Paráda, díky. Škoda, že to v tom návodu není napsáno, začátečníky to může asi dost zmást.
matej21 napsal(a):
Pokud ma komponenta pouze vykreslovat svoji podkompoentu, ktera je formularem, tak bude stacit:
public function render() { $this['articleMenu']->render(); }
Spíš doporučuji mít plnohodnotnou šablonu (já ji pojmenovávám stejně jako komponentu) a v ní jen
{control articleMenu}
Metoda render by vypadala takto (já už na ni mám šablonu v kostře třídy obecné komponenty)
<?php
public function render()
{
$this->template->setFile(__DIR__ . '/articleMenu.latte');
$this->template->render();
}
?>
Editoval Šaman (7. 7. 2014 23:11)
- Šaman
- Člen | 2666
Snadná upravitelnost. Formuláře mám většinou vykreslené defaultně,
než k tomu příjde kodér a najednou mu takové vykreslení nestačí. Teď
musí volat programátora i když by už uměl nahradit
{control fooForm}
makrem {form fooForm}...{/form}
.
Ale samozřejmě je to jen doporučení za základě mých zkušeností, ty máme každý jiné.
- Freestyler
- Člen | 50
Ahoj,
přepsal jsem kód s použitím generovaných továrniček a interfacu
následovně:
IArticleMenuFactory
<?php
namespace App\Components;
interface IArticleMenuFactory {
/** @return ArticleMenu */
function create();
}
Továrnička ArticleMenu
<?php
namespace App\Components;
use Nette\Application\UI\Form;
class ArticleMenu extends \Nette\Application\UI\Control {
/**
* @var App\Model\Articles
* inject
*/
public $articles;
protected function createComponentArticleMenu() {
$form = new Form();
$form->addSelect('category','Kategorie', $this->articles->getArticleCategories()->fetchPairs('id', 'category_name'));
return $form;
}
public function render()
{
$this['articleMenu']->render();
}
}
Model Articles, metoda pro vytažení kategorií z DB.
*
* Get all categories
*/
public function getArticleCategories() {
return $this->database->table('categories');
}
Použití v ArticlesPresenteru
/**
* @var App\Components\IArticleMenuFactory
* @inject
*/
public $articleMenuFactory;
protected function createComponentArticleMenu()
{
return $this->articleMenuFactory->create();
}
První problém je, že kód nefunguje, konkrétně presenter ArticleMenu, tam dostávám Call to a member function getArticleCategories() on a non-object, mám použít konstruktor? Vždyť @inject anotace by měla být identická ne?
Druhý problém je, že tohle všechno dělám proto abych neopakoval kód a měl znovupoužitelnou komponentu. Tzn. třeba v HomepagePresenteru budu muset zase tupě injectovat IArticleMenuFactory, definovat proměnnou $articleMenuFactory a vytvářet komponentu (createComponentArticleMenu()? Je to tak správně, nebo něco vyloženě nechápu na generovaných továrničkách? Nikde jsem nenašel dostatečně vysvětlující příklad pro začátečníka :(.
Díky.
- David Matějka
- Moderator | 6445
První problém je, že kód nefunguje, konkrétně presenter ArticleMenu, tam dostávám Call to a member function getArticleCategories() on a non-object, mám použít konstruktor? Vždyť @inject anotace by měla být identická ne?
defaultne je inject anotace povolena pouze pro presentery. Muzes to
explicitne zapnout pro konkretni sluzbu, ze v neonu k jeji definici uvedes
inject: true
, konkretne:
services:
-
implement: App\Model\IArticleMenuFactory
inject: true
ale je lepsi do komponent injectovat pres konstruktor
Druhý problém je, že tohle všechno dělám proto abych neopakoval kód a měl znovupoužitelnou komponentu. Tzn. třeba v HomepagePresenteru budu muset zase tupě injectovat IArticleMenuFactory, definovat proměnnou $articleMenuFactory a vytvářet komponentu (createComponentArticleMenu()? Je to tak správně, nebo něco vyloženě nechápu na generovaných továrničkách? Nikde jsem nenašel dostatečně vysvětlující příklad pro začátečníka :(.
pokud jednu komponentu pouzivas ve vsech presenterech (nebo v naproste vetsine), presun onu createComponent* metodu do base presenteru. par radku kodu muze take usetrit autowired component factories. pokud komponentu pouzivas pouze v nekterych presenterech a chces se vyhnout copy-pastovani tech createComponent*, muzes (za predpokladu, ze pouzivas php 5.4+) vytvorit traity
Editoval matej21 (17. 7. 2014 0:26)
- Freestyler
- Člen | 50
Komponentu chci používat jen ve dvou presenterech, takže se asi to copy&paste přežije no. Můžu tedy používat jen @inject anotaci a nebo je lepší konstruktor (ten je asi čistější).
Můžeš mi ještě prosím mrknout co by tam tak mohlo být špatně a nette na mě řve s Call to a member function on a non object?
Díky.
matej21 napsal(a):
První problém je, že kód nefunguje, konkrétně presenter ArticleMenu, tam dostávám Call to a member function getArticleCategories() on a non-object, mám použít konstruktor? Vždyť @inject anotace by měla být identická ne?
defaultne je inject anotace povolena pouze pro presentery. Muzes to explicitne zapnout pro konkretni sluzbu, ze v neonu k jeji definici uvedes
inject: true
, konkretne:services: - implement: App\Model\IArticleMenuFactory inject: true
ale je lepsi do komponent injectovat pres konstruktor
Druhý problém je, že tohle všechno dělám proto abych neopakoval kód a měl znovupoužitelnou komponentu. Tzn. třeba v HomepagePresenteru budu muset zase tupě injectovat IArticleMenuFactory, definovat proměnnou $articleMenuFactory a vytvářet komponentu (createComponentArticleMenu()? Je to tak správně, nebo něco vyloženě nechápu na generovaných továrničkách? Nikde jsem nenašel dostatečně vysvětlující příklad pro začátečníka :(.
pokud jednu komponentu pouzivas ve vsech presenterech (nebo v naproste vetsine), presun onu createComponent* metodu do base presenteru. par radku kodu muze take usetrit autowired component factories. pokud komponentu pouzivas pouze v nekterych presenterech a chces se vyhnout copy-pastovani tech createComponent*, muzes (za predpokladu, ze pouzivas php 5.4+) vytvorit traity
- Pavel Macháň
- Člen | 282
Freestyler napsal(a):
Komponentu chci používat jen ve dvou presenterech, takže se asi to copy&paste přežije no. Můžu tedy používat jen @inject anotaci a nebo je lepší konstruktor (ten je asi čistější).
Můžeš mi ještě prosím mrknout co by tam tak mohlo být špatně a nette na mě řve s Call to a member function on a non object?
Díky.
Konstruktor je určitě čistější a neporušuje zapouzdření (@inject musí mít public property). Osobně se snažím používat konstruktor všude (až na BasePresenter).
Editoval Pavel Macháň (17. 7. 2014 10:58)
- Freestyler
- Člen | 50
Ok, díky.
Ještě teda nevím, kde mám chybu v tom kódu, koukám do toho už dobu a pořád to nevidím, popostrčí mě prosím někdo?
Pavel Macháň napsal(a):
Freestyler napsal(a):
Komponentu chci používat jen ve dvou presenterech, takže se asi to copy&paste přežije no. Můžu tedy používat jen @inject anotaci a nebo je lepší konstruktor (ten je asi čistější).
Můžeš mi ještě prosím mrknout co by tam tak mohlo být špatně a nette na mě řve s Call to a member function on a non object?
Díky.
Konstruktor je určitě čistější a neporušuje zapouzdření (@inject musí mít public property). Osobně se snažím používat konstruktor všude (až na BasePresenter).
- Šaman
- Člen | 2666
Anotace @inject funguje pouze v presenteru. Jestli jsi to přepsal, tak netuším, kde jinde máš chybu, protože tu není ten nový kód. Na takovéhle věci je ideální nasdílet projekt na GitHub/BitTracker.
Zkoušel jsi ladit? Řve to proto, že někde nemáš objekt, takže co tam máš? NULL? Pak je chyba v předávání.
- David Matějka
- Moderator | 6445
@Freestyler viz https://forum.nette.org/…nebo-service#…,
pokud chces v komponentach pouzit @inject
, musis to zapnout, jinak
musis pouzit konstruktor
- Freestyler
- Člen | 50
Přepsal jsem to do konstruktoru a funkce z HomepagePresenteru a ArticlesPresenteru (na obou chci zobrazovat to menu pro kategorie). Menu už se vykreslí, ale ještě se peru s tím, jak předat ID kategorie, zkouším to přes konstruktor, ale pořád to nechce sežrat, poradíte prosím co je tam blbě? Koukám do toho 2 hodiny a nevidím to tam :(.
ArticleMenu s interfacem:
<?php
namespace App\Components;
use Nette\Application\UI\Form;
class ArticleMenu extends \Nette\Application\UI\Control {
private $articles, $categoryID;
public function __construct (\App\Model\Articles $articles, \App\Model\Articles $categoryID) {
parent::__construct();
$this->articles = $articles;
$this->categoryID = $categoryID;
}
protected function createComponentArticleMenu() {
$form = new Form();
$form->addSelect('category','Kategorie', $this->articles->getArticleCategories()->fetchPairs('id', 'name'))
->setPrompt('Všechny články');
$form->addSubmit('submit', 'Zobrazit');
$form->onSuccess[] = $this->articleMenuSuceeded;
return $form;
}
public function articleMenuSuceeded($form, $categoryID) {
$this->articles = !empty($categoryID) ?
$this->articles->getByCategoryId($categoryID) :
$this->articles->fetchAll();
$this->redirect('this', ['categoryID' => $form->values->category]);
}
public function render() {
$this['articleMenu']->render();
}
}
interface IArticleMenuFactory {
/** @return ArticleMenu */
function create();
}
V konfigu klasicky – App\Components\IArticleMenuFactory
A data tahám přes model:
<?php
namespace App\Model;
/**
* Description of Articles
*
* @author Freestyler
*/
class Articles extends Base {
/*
* Get all articles from db (limited to 10)
*/
public function fetchAll() {
return $this->database->table('articles')
->limit('10');
}
/**
* Add article to db
*/
public function addArticle() {
}
/**
*
* Get all categories
*/
public function getArticleCategories() {
return $this->database->table('categories');
}
/*
* Get specific article based on category_id
*/
public function getArticleByCategoryId($categoryID) {
return $this->database->table('articles')
->where('categories_id', $categoryID);
}
}
Problém bude v předávání toho parametru a ještě si nejsem jistej, jestli mám správně napsanou funkci articleMenuSuceeded, která má vzít data z formu, vytáhnout ID z databáze podle toho který je vybraný a zobrazit články, který odpovídají tomu ID.
Díky.
- Šaman
- Člen | 2666
public function __construct (\App\Model\Articles $articles, \App\Model\Articles $categoryID) {
WTF? Takhle se předávají služby, takže teď to do
$categoryID
předá stejnou instanci jako do articles. Parametr si
musíš predávat ručně, ideálně v tovární metodě v presenteru. Buď si
najdi, jak předávat parametr metodě create()
(budeš muset
upravit rozhraní a config), nebo si na to $id napiš setter a v tovární
metodě createComponent… si po vytvoření komponenty pomocí služby ještě
zavolej ten setter.
- Freestyler
- Člen | 50
Ok, přepsal jsem to následovně:
Konstruktor:
private $articles;
public function __construct(\App\Model\Articles $articles, $categoryID) {
parent::__construct();
$this->articles = $articles;
$this->categoryID = $categoryID;
}
Interface:
interface IArticleMenuFactory {
/** @return ArticleMenu */
function create($categoryID);
}
Config.neon
-
implement: IArticleMenuFactory
parameters: [categoryID]
arguments: [%categoryID%]
Vyhodí mi to chybu Missing item $categoryID, což moc výmluvný není a nikde jsem k ní nic nenašel. Tohle je pro mě španělská vesnice, tak bych potřeboval nakopnout asi víc, díky.
Šaman napsal(a):
public function __construct (\App\Model\Articles $articles, \App\Model\Articles $categoryID) {
WTF? Takhle se předávají služby, takže teď to do$categoryID
předá stejnou instanci jako do articles. Parametr si musíš predávat ručně, ideálně v tovární metodě v presenteru. Buď si najdi, jak předávat parametr metoděcreate()
(budeš muset upravit rozhraní a config), nebo si na to $id napiš setter a v tovární metodě createComponent… si po vytvoření komponenty pomocí služby ještě zavolej ten setter.
- Freestyler
- Člen | 50
Dal jsem to jako první a beze změny :(. Hodil jsem ten kód na pastebin (je tam model, HomepagePresenter, config a ten interface). Kdyby jsi na to mrknul byl bych dost vděčnej, protože už jsem zkusil x nastavení a konfigurací z různých příspěvků a pořád jsem seknutej na tom samým.
Děkuji.
- Šaman
- Člen | 2666
Sem musíš ještě předat to $id. Kde jinde by ho aplikace vzala? Celé to přepisování bylo kvůli tomu, abys naučil metodu create akceptovat parametr. A presenter už by měl vědět, pro jakou kategorii tu komponentu vytváří.
<?php
protected function createComponentArticleMenu()
{
return $this->articleMenuFactory->create($categoryId);
}
?>
- Freestyler
- Člen | 50
Bohužel pořád ta samá chyba „Missing item "categoryID“. Tam bude problém spíš někde v configu, ale zkoušel jsem to přepsat na jiné způsoby definice a je to stejné. Fakt si s tím už nevím rady :(.
- Šaman
- Člen | 2666
Tak se pro začátek vyprdni na generované továrničky a napiš si ji po svém (ještě mě napadlo jakou máš verzi Nette, protože ještě nedávno se tyhle složitější s parametrem musely psát ručně).
Tzn. Místo interface IArticle…Factory budeš mít třídu
Article…Factory, která bude obsahovat metodu
create($categoryId)
, uvnitř bude obyčejné vytvoření pomocí
operátoru new, nastavení toho $id a vrátíš nakonfigurovanou instanci.
Tuhle tovární třídu si zaregistruješ v configu jako službu (takže do
konstruktoru můžeš normálně předávat závislosti) a pak ji předáš do
presenteru a použiješ jako doteď. Jinak řečeno v configu budeš mít
services:
- App\Model\ArticleMenuFactory
a v presenteru ji budeš injectovat bez toho I.
Pak se ti to bude lépe ladit a taky pochopíš, co vlastně pak dělá ta generovaná továrna. Jen ti ušetři trochu psaní.
Jinak v tom pastbinu máš špatně odsazené
-
implement: IArticleMenuFactory
parameters: [categoryID]
arguments: [%categoryID%]
ale předpokládám, že to by házelo jinou chybu a že to máš asi jen špatně vykopírované.
Edit: Resp. úplně nejjednodušší bude pro začátek nemít žádnou
tovární třídu a prostě si tu komponentu vytvořit v presenteru
v createComponent… pomocí new. (Repozitář si injectneš do presenteru a
předáš jako parametr do konstruktoru).
Tohle odladíš a pak si vytvoříš tu tovární třídu (ta se dělá proto,
abys nevytvářel tu komponentu na více místech pokaždé jinak).
A pokud budeš chtít (z hlediska čistoty návrhu už ti to nic nepřinese),
tak pak můžeš přepsat továtní třídu na generovanou.
Editoval Šaman (21. 7. 2014 14:41)