Komponenta v modelu, nebo service?

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

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

ComponentFactory musi byt komponenta, takze musi dedit Nette\Application\UI\Control a musi mit render metodu. (+ to pojmenuj rozumeji)

Freestyler
Člen | 50
+
0
-

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
+
+1
-

Pokud ma komponenta pouze vykreslovat svoji podkompoentu, ktera je formularem, tak bude stacit:

public function render()
{

$this['articleMenu']->render();
}
Freestyler
Člen | 50
+
0
-

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
+
0
-

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)

Majkl578
Moderator | 1364
+
0
-

Šaman napsal(a):

Spíš doporučuji mít plnohodnotnou šablonu (já ji pojmenovávám stejně jako komponentu)

Jaká je přidaná hodnota? Je to víc psaní, nemluvě o exekuci šablony a Latte navíc…

Šaman
Člen | 2666
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

@Freestyler viz https://forum.nette.org/…nebo-service#…, pokud chces v komponentach pouzit @inject, musis to zapnout, jinak musis pouzit konstruktor

Freestyler
Člen | 50
+
0
-

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
+
0
-

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
+
0
-

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.

Šaman
Člen | 2666
+
0
-

A ta továrnička v konstruktoru? Předáváš ji parametr? Jo a to $categoryID dej jako první parametr konstruktoru, až pak ty autowirovaný.

Freestyler
Člen | 50
+
0
-

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.

http://pastebin.com/wjgmCz35

Děkuji.

Šaman
Člen | 2666
+
0
-

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
+
0
-

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
+
0
-

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)