Autor Zpráva
Ntt
Profil *
Dobrý den, začínám ted trochu s nette a chci se zeptat na modely. Našel jsem pár ukázkových aplikací, ale zde jsou v modelech pouze metody pro práci s databází, tedy skoro žádný objektový přístup. Navíc do každé třídy musím do kontruktoru vkládat Context, tu třídu musím registrovat jako službu...
čili ty modely vypadají zhruba takto:
class ArticleModel{

   public function __construct(Nette\Database\Context $db) 
   
   public function save(array $data);
   
   /** @return array */
   public function getArticle($id);
   
   /** @return array */
   public function getComments($idArticle);
}
v config.neon musím mít:
services:
    articleModel: App\Model\ArticleModel

Volání potom vypadá takto:

$article = $this->context->articleModel->getArticle($id);
$comments = $this->context->articleModel->getComments($article["id"]);

To je ale dost nepříjemné. jak to řešíte vy? jak se dá vytvořit objektový model? našel jsem řešení jako že budu mít ArticleRepository, který bude dostávat context a pak třídu ArticleModel, která bude volat metody ArticleRepository a bude mít v konstruktoru taky context a předávat ho do repository. to mi ale také nepřipadá úplně ideální.
Budu rád když se se mnou podělíte o své zkušenosti
Díky
Zechy
Profil
Ntt:
Já mám vytvoření tzv. modelsContainer, ve kterém si vytvářim Database\Connection, pokud ještě ve třídě nebyl vytvořen, a v něm pak mám funkce pro vytvoření jednotlivých modelů. Když potřebuju článek, tak si zavolám z něho:
$modelsContainer->createArticle()
Ten container mám samozřejmě vytvořený už v BasePresenteru v konstruktoru, ať ho mám vždy k dispozici.
Jinak v praxi vypadá asi takhle:
class ModelsContainer extends Nette\DI\Container {
    private $services = array();

    private function createConnection() {
        include "app/config/database.php";
        $dns = "mysql:host=".host.";dbname=".dbname;

        return new Nette\Database\Connection($dns, user, password);
    }

    public function getConnection() {
        if(!isset($this->services["connection"])) {
            $this->services["connection"] = $this->createConnection();
        }

        return $this->services["connection"];
    }

    public function createArticle() {
        return new Article($this->getConnection());
    }
}
Je to vlastně zkopírované řešení z nette dokumentace.

Případně, pokud potřebuješ vytvořit Context, dá se to napsat stejným stylem, konstruktor contextu příjímá Nette\Database\Connection.
Ntt
Profil *
To je zajímavé řešení, ale každý vytvořený model, musím přidávat do této třídy. navíc každý model bude muset do konstruktoru dostávat connection. co když chci mít v konstruktoru název článku, id...
tak aby metoda getArticle vracela něco jako return new Article($row->id, $row->title...);
Zechy
Profil
Ntt:
Tak dáš funkci createArticle parametr pro ID, který pak předáš konstruktoru.
Ntt
Profil *
no pořád se mi to moc nelíbí. ty modely v nette jsou stavěný hlavně jenom na práci s databází a neřeší příliš OOP a když tak to jsou celkem divoká řešení
Zechy
Profil
Ntt:
Nevim jak jinak by sis to představoval, ať už to vytváříš pomocí jiné třídy nebo jako službu, tak vždycky někam musíš ten model zapsat. Automaticky se to samo nevytvoří.
Moje řešení je přímo opsané z Nette dokumentace, kde to ukazují jako správné řešení jak toto řešit.
Ntt
Profil *
a jak to vypadá v konfigu? kterou třídu registruješ jako službu a jak se to potom volá v presenteru?
Zechy
Profil
Ntt:
V configu nic neregistruju, todle řešení jsem použil už právě kvůli tomu, že to nikam psát nemusim, jenom vytvářím instanci. V presenteru, protože má v use nastavenou namespace App\Model, vytvářim třídu takto:
$this->modelsContainer = new Model\ModelsContainer;
Samozřejmě v BasePresenteru mám vytvořenou protected proměnnou $modelsContainer.
Ntt
Profil *
tak ten ModelContainer by mohl být už vytvořený v base presenteru ne?


poslal bys mi odkaz na tu dokumentaci kde je tohle popsaný? díky
Zechy
Profil
Nette: Dependency Injection
Ntt
Profil *
díky za rady. kdyby někdo používal jiné řešení budu rád když se podělíte


Hele tak ještě jeden příklad. Mám třídu UserManager, ta je v sandboxu už definovaná, stará se o autentizaci. No a já do ní přidal metodu getUser, která by měla vrátit instanci třídy User a předat mu jeho data. Dále chci už pracovat jenom s objektem User, ten bude mít metodu edit data a podobně..
Tím tvým řešením bude pořád muset mít v konstruktoru nějaký connection. já prostě nechci aby user byl tak závislý na databázi. nešlo by třeba aby modely dědily od nějakého společného předka connection?

Když jsem nepoužíval nette, tak jsem měl nějaké třídy které pracovali s databází (DbReader, DbWriter) a ty jsem si vytvářel v metodách kde byly potřeba, tím se mi tam skryly, navíc jsem měl zajištěno že vždy mají connection. možná to nebylo ideální vytvářet instanci v každý metodě...

Tohle řešení s DI se mi taky zrovna dvakrát nelíbí no, ale asi jiná možnost není. přijde mi že mě to v OOP dost omezuje...
Alphard
Profil
Ten kód v [#1] Ntt ukazuje použití služeb. To už je specifická aplikace. Při prvním vyžádání se vytvoří jedna instance a ta je předávána při všech dalších voláních. Tohle má smysl právě třeba u UserManager, nehodí se pro reprezentaci konkrétních dat, kde může např. existovat více instancí současně.
Samozřejmě nic nebrání tomu, aby se v případě potřeby vedle služeb normálně vytvářely nové instance objektů new XXX. Právě naopak, předpokládá se to.

už pracovat jenom s objektem User, ten bude mít metodu edit data a podobně
Tím tvým řešením bude pořád muset mít v konstruktoru nějaký connection
Ano, to je podstata DI. Je třeba položit si otázku, má třída User závislost na databázi? Pokud ano, je zde závislost a musí se předat connection. (Že to někteří hackují pomocí inject a následné reflexe je věc druhá.)

Té závislosti na db je možné zbavit se třeba tak, že User nebude databázi potřebovat, ale bude si nějak vnitřně reprezentovat data, která budou následně uložena pomocí UserManager. V pseudokódu nějak takto UserManager->save(User).

Jestli vám jde o mapování dat z databáze na objekty, podívejte se na www.doctrine-project.org.
O principu 5vrstvém modelu víte? Já to třeba nepoužívám, ale je dobré to znát.

Zechy [#2][#10]:
To je jen vysvětlující článek pro funkci kontejneru, který Nette implementuje tak, jak bylo uvedeno v [#1] Ntt, ne?
aDAm
Profil
ale fuj používat $this->context....pokud jdeš cestou DI tak si holt ty závislosti musíš předat v tom je ta podstata.
Ntt
Profil *
aDAm:
ale fuj používat $this->context
A jak mám teda volat služby?

Alphard:
Samozřejmě nic nebrání tomu, aby se v případě potřeby vedle služeb normálně vytvářely nové instance objektů new XXX. Právě naopak, předpokládá se to.
No jo, ale jak do nich tedy dostanu Context? napadá mě že bych měl nějakou hlavní třídu Model, tam vytvořím context a modely by ho potom dědily. když je to služba, tak context dostane automaticky...
Já v tom prostě pořád mám trochu zmatek. nechci bastlit nějaké prasárny...
Jan Tvrdík
Profil
Ntt:
V prvé řadě – nejsou žádné modely. Model je v aplikaci jeden a jedná se o vrstvu aplikace, obvykle složenou z více tříd. To, co ty pravděpodobně hledáš, jsou entity, tedy zjednodušeně řešeno objekty, které představují jednotlivé řádky v databázi. Pokud chceš, aby entity měli metody jako save, tak hledáš návrhový vzor, který se jmenuje active record. Tento návrhový vzor je považovaný za antipatern, ale pro jednodušší aplikace se docela hodí.


Ntt:
A jak mám teda volat služby?
Přečti si doc.nette.org/cs/2.1/di-usage
Ntt
Profil *
nene active record použít nechci. mám na mysli modely = třídy ve vrstvě model, asi sjem to špatně napsal.
Jan Tvrdík
Profil
Ntt:
Pokud nepoužíváš (nechceš používat) active record, tak není žádný důvod, proč by entita (např. zmíněný User) měla mít závislost na připojení k databázi.

Jako ORM můžeš kromě zmíněné Doctrine použít taky Lean Mapper.
Ntt
Profil *
No právě proto, že by se mi líbilo v UserManager zavolat getUser(), získal bych objekt uživatele a ten by měl třeba metodu getArticles() která by mi vrátila všechny články toho uživatele. nebo by tam byla třeba metoda edit(), která by editovala danného uživatele bez toho aniž by v parametru muselo být jeho id, to má už User v sobě.
Když to budu dělat pomocí UserManager tak to bude takhle:
$user = $this->userManager->getUser($id);
$this->userManager->edit($user);
Ntt
Profil *
Zkusil jsem narychlo vytvořit jedno řešení, možná by chtělo ještě trochu vylepšit:

Třída model/storage/mysql/MySqlStorage
namespace App\Model\Storage;

abstract class MySqlStorage extends \Nette\Object implements IMySqlStorage{
    private $database;
    
    /** @var \Nette\Database\Connection */
    private $connection;
    
    /**
     * @return \Nette\Database\Context
     */
    protected function getDatabase(){
        if($this->connection == null)
            $this->connection = $this->createConnection();
        $this->database = new \Nette\Database\Context($this->connection);
        return $this->database;
    }
    
    private function createConnection(){
        return new \Nette\Database\Connection(DSN, DBUSER, DBPASSWORD);
    }
}

Interface model/storage/mysql/IMySqlStorage:
namespace App\Model\Storage;

interface IMySqlStorage {
    
    function save(array $data);
    
    function read($id);
    
    function exists($id);
    
    function delete($id);
    
    function readAll();
    
    function deleteAll();
}

Třída model/storage/mysql/UserMySqlStorage:
namespace App\Model\Storage;

class UserStorage extends MySqlStorage{
    
    public function delete($id) {
        
    }

    public function deleteAll() {
        
    }

    public function exists($id) {
        
    }

    public function read($id) {
        
    }

    public function readAll() {
        return $this->getDatabase()->table("users")->fetchAll();
    }

    public function save(array $data) {
        
    }

}

Třída model/User:
namespace App\Model;

class User extends \Nette\Object{
    
    /** @var App\Model\Storage\UserStorage */
    private $storage;
    public function __construct() {
        $this->storage = new \App\Model\Storage\UserStorage();
    }
    
    public function save(){
        
    }
    
    public function readAll(){
        dump($this->storage->readAll());
    }
}

Co na to říkáte? samozřejmě ta metoda readAll() by spíš měla být jako statická třeba v UserManager. to už je jedno. každá třída at už UserManager, ArticleManager bude mít svůj Storage. Ještě je možnost že by tyto třídy ke Storage přistupovali přes nějaký interface, třeba přes IUserStorage, ale to zatím nevím jak implementovat.
aDAm
Profil
že toto MySqlStorage je docela fuj ;) proč si tam tu connection nepředáš přes konstruktor?
Ntt
Profil *
A kde jí pak vezmu? stejně by connection potom museli vytvářet potomci, tak není lepší to vytvářet na jednom místě než na 10?
Alphard
Profil
Pokud instanci třídy User vytváří UserManager (což by bylo docela rozumné řešení), může connection předat, protože je sám má. A když si tu instanci budete vytvářet sám někde v presenteru, tak se k connection taky dostanete..., není to ideální, ale lepší, než aby si třídy vyčarovaly připojení z ničeho.

Ntt:
tak není lepší to vytvářet na jednom místě než na 10?
Z pohledu pracnosti je a někdy se to dělá. Ale z pohledu návrhu je to antipattern, protože je tam skrytá závislost. Představte si, že si stáhnete nějako komponentu, která bude ve svém kódu magicky sbírat různé závislosti z autora prostředí. Myslíte, že vám bude fungovat? Nebude, budete to záplatovat ještě týden podle toho, jakou chybu to zrovna hodí. Proto ta snaha jasně deklarovat závislosti.
Ntt
Profil *
Takže se mám MySqlStorage zbavit a vytvářet spojení v každé třídě zvlást?
Alphard
Profil
Ntt:
a vytvářet spojení v každé třídě zvlást?
Jen to ne.
Praxe je plná kompromisů, ale ať už budete s distribucí connection dělat cokoliv, zajistěte, ať se všude používá pouze jedno připojení. Určitě nevytvářejte nové připojení v každé třídě (jak to dělá výše uvedený kód ukládající do nestatické proměnné), to by vám snižovalo výkon.
Ntt
Profil *
takže v MySqlStorage mít statickou promennou connection, v té třídě pouze navázat spojení a v ostatních storage vytvářet Context?
Jan Tvrdík
Profil
Ntt:
A kde jí pak vezmu?
Pokud se ptáš, kde vzít závislost, tak jsi zřejmě nepochopil koncept dependency injection. Závislosti si nikdy nebereš, závislosti pouze dostáváš (obvykle konstruktorem). Přečti si např. sérii článků o DI na phpfashion.

takže v MySqlStorage mít statickou promennou connection
Ne, statické proměnné je dobré vůbec nepoužívat.

Třída MySqlStorage by mohla vypadat třeba takto:

abstract class MySqlStorage extends \Nette\Object implements IMySqlStorage {
    /** @var \Nette\Database\Context */
    protected $database;

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

Taky je nesmysl vytvářet v každé instanci User novou instanci UserManager. Tu instanci si normálně předej konstruktorem:
class User extends \Nette\Object{
    /** @var UserStorage */
    private $storage;

    public function __construct(UserStorage $storage) {
        $this->storage = $storage;
    }
}
Ntt
Profil *
Díky za radu. to mi pomůže. O DI jsem četl dost, ale všude psali jenom že mám do konstruktoru předat tu závislost a dál to neřešit. ale přece framework sám od sebe nemůže zjistit že je tohle zrovna závislost. to budu muset uvést v konfigu ne? nebo ta třída musí od někoho dědit..
Nebo pokud UserStorage budu používat v User, tak tam mu musím tu závislost předat.
a proč je nesmysl vytvářet UserStorage uvnitř? já vím skrytá závislost, ale mě to zase jako takový problém nepřipadá. když mám třídu user, která má metodu save a metodu read, tak vím že když zavolám save, user se uloží a když zavolám read tak se načte a co se děje uvnitř vědět nepotřebuju, když bych šel do důsledku tak je skrytá závislost každá funkce která se ve třídě volá...


Ta třída User cos mi poradil, já v tom moc výhodu nevidím, omlouvám se asi jsem natvrdlý, určitě je to dobře, ale přijde mi to složité. v presenteru budu muset mít injectContext, potom vytvořit instanci UserStorage a ten předat tříde User, to už se mi nevyplatí dělat zapouzdření User a radši používat UserStorage...
Zechy
Profil
Jsem si zpětně vzpomněl, že tu mám nezodpovězený dotaz :-).

Alphard:
To je jen vysvětlující článek pro funkci kontejneru, který Nette implementuje tak, jak bylo uvedeno v [#1] Ntt, ne?
JE fakt, že to implementuje jako službu, já si ovšem kontejner upravil tak, že si vytvářím vlastní instanci Nette\Connection\Database z vlastního konfiguračního souboru. Sestrojil jsem si vlastní DNS pro připojení. (mysql:host=<host>:dbname=<nazev_databaze>, username a heslo příjímá jako další parametry Database()).

Vaše odpověď

Mohlo by se hodit


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm: