Čo je to model a načo slúži?
- Čamo
- Člen | 798
Dobrý večer,
ja by som potreboval vysvetliť jednu vec, ktorá možno nesúvisí s Nette,
ale ja myslím, že na vine je NDBT.
Snažím sa vytvoriť model.
Napísal som niečo takéto:
namespace App\Model;
use Nette,
Nette\Diagnostics\Debugger;
class GeneralModel
{
/** @var Nette\Database\Context @inject */
public $database;
public function __construct(Nette\Database\Context $db)
{
$this->database = $db;
}
/**
* @param string $table
* @param string|NULL $condition
* @param string|NULL $order
* @param int|NULL $limit
* @param int|NULL $offset
*
* @return selection
*/
public function getTable($table, $condition=NULL, $order=NULL, $limit=NULL, $offset=NULL)
{
$table = $this->database->table($table);
if($condition)
{
$table->where($condition);
}
if($order)
{
$table->order($order);
}
if($limit)
{
$table->limit($limit, $offset);
}
return $table;
}
}
Volanie takéhoto niečoho vyzerá takto:
$selection = $this->genModel->getTable('users', NULL, NULL, 20, 20);
podľa mňa to vyzerá s tými NULLmi otrasne a rozmýšľal som, že použiť priamo zápis
$selection = $this->database->table('users')->limit(20,20)
je oveľa elegantnejšie a keby som potreboval zložitejšiu podmienku tak aj
výhodnejší.
Úplne sa mi stráca zmysel celého modelu. Prečo to mám vlastne robiť, keď
ten druhý zápis je kratší, elegantnejší a dáva mi viac možností?
Nechápem to.
Vie mi niekto vysvetliť čo som nepochopil?
Editoval Čamo (5. 8. 2014 21:55)
- japlavaren
- Člen | 404
cau,
funkcia ako ju mas napisanu getTable nedava zmysel. model je o tom, ze mas
zapuzdrene a na jednom mieste kod, ktory pri potrebnej zmene lahko upravis (a
nie hladat napriec celym projektom, kde som to len pouzil).
okrem ineho ak nastane chyba, mozes sa rozhodnut co s nou (zalogovat, poslat
dalej) a co vratis do presenteru
pozri sa sem, pisem tam nieco malo o tom, ako robim modely ja https://forum.nette.org/…principy-mvc#….
ak uspesne dokoncim do konca mesiaca projekt, mam v plane natocit nejake videa
o novom nette bo je v tom bordel a chybaju zakladne veci
- Čamo
- Člen | 798
@japlavaren
No veď podľa toho tvojho príkladu som to robil. S modelom robím prvý
krát, tak som z toho zmetený. Nejaký model už mám napísaný, ohľadom
prihlasovania a registrácie. Ten zmysel dáva. Odchytáva výnimku, overuje
uživateľa. A už asi niečo začínam cítiť. Tie vzniknuté chyby sú
dobrý dôvod.
Díky.
PS:Tie videá rýchlo natoč. Projekt môže počkať :)
- mystik
- Člen | 312
Čamo: Zkus to položit takhle. Proč potřebuješ zjistit že ty data nejsou false? To má nějaký logický význam v aplikaci. Model slouží k tomu, aby si aplikace řekla o to co ji zajímá a neřešila jak se to zjistí. Protože pak třeba zjistíš, že se to musí zjišťovat o trochu složitěji (např. zajímají tě jen uživatelé u kterých jsi nedal flag že jsou neaktivní). Pokud máš model upravíš jeden řádek. Pokud ho nemáš musíš projít kompletně celou aplikaci a najít úplně všechny místa, kde to zjišťuješ.
Zkus sem hodit příklad Controlleru, kde ten tvůj model používáš (třeba tu registraci).
Editoval mystik (6. 8. 2014 9:31)
- Čamo
- Člen | 798
@mystik
Hej to čo píšeš dáva zmysel.
Ten môj registerPresenter vyzerá takto:
<?php
namespace App\Presenters;
use Nette,
Nette\Security\Passwords,
App\Model\UserModel;
/**
* Registration presenter.
*/
class RegistPresenter extends \App\Presenters\BasePresenter
{
/** @var App\Model\UserModel */
private $userModel;
public function __construct(UserModel $userModel)
{
$this->userModel = $userModel;
}
/**
* Registration form factory
* @return Nette\Application\UI\Form
*/
protected function createComponentRegistForm()
{
$form = new Nette\Application\UI\Form;
$form->addText('username', 'User name:')
->setRequired('Vyplňte prosím meno.')
->setAttribute('class', 'formEl');
$form->addPassword('password', 'Password:')
->setRequired('Zadajte prosím heslo.')
->addRule($form::MIN_LENGTH, 'Zadajte prosím heslo s minimálne %d znakmi', 3)
->setAttribute('class', 'formEl');
$form->addPassword('password2', 'Password check:')
->setRequired('Zadajte prosím heslo.')
->addRule($form::EQUAL, 'Heslá sa nezhodujú. Zopakujte prosím kontrolu.', $form['password'])
->setAttribute('class', 'formEl');
$form->addText('email', 'Email:')
->setRequired('Zadajte prosím emailovú adresu.')
//->addCondition()
->addRule($form::EMAIL, 'Nezadali ste platnú mailovú adresu. Opravte si prosím chybu.', $form['password'])
->setAttribute('class', 'formEl');
$form->addSubmit('send', 'Registrovať');
$form->onSuccess[] = $this->registFormSucceeded;
return $form;
}
public function registFormSucceeded($form)
{
$values = $form->getValues();
$hash = Passwords::hash($values['password']);
$arr = array('password' => $hash,
'username' => $values['username'],
'email' => $values['email'],
'status' => 10,
'created' => time()
);
try
{
$user = $this->userModel->RegisterUser($arr, 'users', $values);
}
catch(\App\Model\Exceptions\DuplicateEntryException $e)
{
$form->addError($e->msg);
return;
}
$this->userSess->username = $user['username'];
$this->userSess->id = $user['id'];
$this->userSess->created = $user['created'];
$this->userSess->status = $user['status'];
$this->flashMessage('Vitajte '.$values['username'].'. Vaša registrácia bola úspešná a loli ste automaticky prihlásený(á).');
$this->redirect(':Default:');
}
}
Neviem, či to hashovanie patrí do presentera, či do modelu…
A model vyzerá:
namespace App\Model;
use Nette,
Nette\Security\Passwords,
Nette\Diagnostics\Debugger;
/**
* @method RegisterUser
*/
class UserModel
{
/** @var Nette\Database\Context @inject */
public $database;
public function __construct()
{
}
/**
* @param array $params
* @param string $table
*
* @return activeRow
*
* @throw Nette\InvalidArgumentException
* @throw Model\Exceptions\DuplicateEntry
*/
public function RegisterUser($params, $table, $values)
{
if(!is_array($params))
{
throw new Nette\InvalidArgumentException('$params must be an array');
}
try
{
$row = $this->database->table((string)$table)->insert($params);
}
catch(\PDOException $e)
{
// This catch ONLY checks duplicate entry to fields with UNIQUE KEY
$info = $e->errorInfo;
// mysql==1062 sqlite==19 postgresql==23505
if ($info[0] == 23000 && $info[1] == 1062)
{
$db = $this->database;
// if/elseif returns the name of problematic field and value
if( $db->table('users')->where('username = ?', $values['username'])->fetch() )
{
$msg = 'Meno '.$values['username'].' je už obsadené. Vyberte si prosím iné.';
}
elseif( $db->table('users')->where('email = ?', $values['email'])->fetch() )
{
$msg = 'Email '.$values['email'].' je už zaregistrovaný. Musíte uviesť unikátny email.';
}
throw new Exceptions\DuplicateEntryException($msg);
}
else throw $e;
}
return $row;
}
/**
* This method checks if user exists
* @param array
* @param string
*
* @return DB row or false
*/
public function SignInUser($values, $table)
{
$row = $this->database->table($table)->where('username = ?', $values['username'])->limit(1)->fetch();
if($row)
{
if(Passwords::verify($values['password'], $row['password']))
{
return $row;
}
}
// here MUST NOT be else!!!
return false;
}
Editoval Čamo (6. 8. 2014 21:32)
- Šaman
- Člen | 2666
Tohle ti funguje? Co máš za verzi Nette? V 2.2 už se nedá injectovat do tříd modelu jinak, než přes konstruktor, anotace a inject metody fungují jen v Presenteru.
Hashování do modelu patří.
Nechápu to
$this->userModel->RegisterUser($arr, 'users', $values);
To, že se má zapisovat do tabulky 'users'
je interní věc
modelu. Presenteru do toho nic není. PResenter má za úkol načíst data a
předat je modelu. Model má za úkol zařídit, aby ta data uložil. Jestli to
zapíše do tabulky users
, foo_bar
, nebo uloží
serializovanou instanci User
na disk, to je presenteru jedno.
Presenter zkrátka zná API userModel
u a s ním pracuje.
Ideální by bylo něco takového:
$this->userService->register($values);
P.S. Model není dobrý název třídy, ale budiž. Ideální je, pokud máš
spodní vrstvu modelu, repository, která se stará jen o CRUD. Nad ní jsou
vrstvy service
, nebo taky facade
, které se starají
o komplexnější činnosti, jako třeba právě registrace.
Teprve když model rozvrstvíš, tak začne být přehledný a začne
dávat smysl.
- Čamo
- Člen | 798
@Šaman
- Ano ten inject nefunguje, kôli prehladnosti som zmazal konštruktor a bez neho je to blbosť
- To hashovanie som premiestil do modelu takže sa volá registerUser($values, $table);
- Nemôže to tak byť? Nemôžu existovať dve tabuľky, v ktorých by mohli byť usery registrovaní? Napr. modul Blog a modul Eshop budú mať zvlášť tabuľky. Či mal by mať každý modul vlastný model?
- Neviem čo je CRUD(zistím) ja mám zatiaľ len BaseModel a *Model
- Nerozumiem prečo nie model ale repository. Som asi hold zelenáč…
Díky
Editoval Čamo (6. 8. 2014 11:20)
- Šaman
- Člen | 2666
- 3. Každý modul vlastní model, pokud bude každý modul chtít tahat uživatele z jiné tabulky.
- 4. CRUD, základní operace. Vyšší vrstvy by si také neměly sahat do databáze samy, ale přes vrstvu (často se jmenuje xxxRepository), která implementuje tyto operace. Případná změna úložiště pak postihne jen úpravu repository, vyšší vrstvy si toho ani nevšimnou.
- 5. Model je celá část architektury aplikace. Pokud máš jen jednu třídu pro každou entitu, tak tomu asi můžeš říkat model (používá to i David v Sandboxu), ale jakmile budeš model dále dělit, tak už název třídy xxxModel ztrácí smysl – model jsou všechy třídy modelu. Co se týče dělení modelu do vrstev, zkus si najít třeba přednášku o pětivrstvém modelu. Reálně ale zatím budeš používat jen něco, čemu já říkám 1½, nebo 2½ vrstvý model (ta půlka je NDb, nebo Dibi, jako univerzální dotazovač, který zvládá komunikovat s různými databázemi).
Editoval Šaman (6. 8. 2014 11:38)
- Čamo
- Člen | 798
3. To ma asi ešte čaká
4. Tú spojenie s databázou dám do baseModelu a ten CRUD asi zatiaľ
v takomto jednoduchom modeli nemá zmysel vyčleniť.
5. Tak zatiaľ model keď aj vedúci to tak píše.
Díky za obšírne vysvetlenie.
PS: ako som to zmenil potrebujem otestovať
is_array($values), čo samozrejme nefunguje.
Prepísal som to na $values instanceof Nette\Utils\ArrayHash ,
ale niesom si istý, či je to správne(či je dosť univerzálne pre
akceptáciu polí).
Editoval Čamo (14. 10. 2014 21:24)
- Jan Suchánek
- Člen | 404
@Čamo Mrkni třeba na YetORM tam jě to moc pěkný. Podle mě používáš uplně zbytečný if else hell.
Editoval jenicek (6. 8. 2014 12:35)
- Jan Suchánek
- Člen | 404
@Čamo jeste jedna vec jak registrujes, zda email co uzivatel zadal je opravdu funkcni email? Ja ty veci resim eventama vcetne flash message apod.
- japlavaren
- Člen | 404
este ma napada jeden konkretny priklad nutnosti modelu:
mam vypis (inzeratov) a na stranke mam tento vypis niekolko krat (uvod,
kategorie, inzeraty uzivatela). modelu predam len filter, podla ktoreho ma
filtrovat a ten sa postara o vsetko.
Vyhody:
- ak zmenim nazov stlpca (pridam novy a stary sa bude pouzivat na nieco ine) – zmenim to len v modeli
- vo filtry nastavim len podla coho filtrovat, poradie filtrov (tj. aby bol select na db co najefektivnejsi) riesi model a ja nad nim nemusim rozmyslat, v akom poradi ich budem pisat
- ak potrebujem novy filter zaregistrujem ho v modely a pouzivam kde potrebujem a neriesim to poradie z predchadzajuceho bodu (aspon ja si po pol roku nepametam a musim to dohladavat, ak nemam dobre spraveny model)
- Čamo
- Člen | 798
@japlavaren
Hej ten filter je určite lepší ako ten môj nápad s NULLmi v parametroch.
Ja som sa chcel vyhnúť skladaniu toho filtra v presentery. Ale asi nejde mať
všetko dokonalé.
Díky, že sa snažíte mi to vysvetliť.
Keby niekto videl nejakú problematickú časť v tom mojom UserModeli, tak to by som uvítal. Na príklade sa to ľahšie pochopí. Už som podľa Šamana presunul DB do BaseModelu a tiež hashovanie hesiel do modelu.
@jenicek
Pokiaľ viem tak email sa nedá takto overiť. Ale toto tu nemôžeme rozvíjať
je to príliš OUT.
Radšej mi vysvetli, čo myslíš pod tým if-else hell. Lebo je si neviem
predstaviť, že by som niečo vyhodil.
Editoval Čamo (6. 8. 2014 21:06)
- BigCharlie
- Člen | 283
And that's how new @radvis was born…
Kdybys potřeboval návod, jak hledat na google, jeden už suším v šuplíku.
- Jan Suchánek
- Člen | 404
@Čamo Myslim hned metodku RegisterUser testovani zda jde o array bych dal pryc a misto toho bych napsal array primo k parametru metody. Table je tam uplne zbytecne melo by byt jako parametr tridy, ktera vlastni tuto metodu atd.
Proste zkratil bych tem metodam zodpovednost na minimum, a rozdelil na metod.
- Jan Suchánek
- Člen | 404
Přesně abys věděl co co dělá a mohl si kousek použít i jinde.
A hlavně mrkni na ten YetOrm mě
příjde dobrej, jednoduchej, přehlednej,
vím že existuje LeanMapper a je lepší o moc, ale moc jsem mu nepřišel
zatím na zub.
Muj rychlej nástřel jak bych to přibližně s Nette\Database\Context psal.
function insert(array $values)
{
return $this->getTable()->insert($values);
}
function findEmailOrUsername($email, $username)
{
return $this->getTable()->where("email = ? or username = ?", $email, $username)->fetch();
}
function register(array $values)
{
try {
return $this->insert($values);
} catch(\PDOException $e) {
if($this->fineEmailOrUsername($values["email"], $values["username"]){
throw new Exceptions\DuplicateEntryException;
/*
V presenteru bych ji zachytil a napsal bych flashmessage až tam
navíc tam máš dostupné values tak si tam můžeš napsat co chceš.
"Zadaný email $values["email"] nebo uživatelské jméno $values["username"]
je již registrované."
Protože za tyhle hlášky by neměla být zodpovědná vyjímka ani model.
Model má vykonat jen úlohu co po něm chceš a ne aby ještě něco vypisoval,
zjištoval a rozhodoval.
Rozhodovat má presenter nebo komponenta od toho tu jsou.
*/
}
throw $e;
}
}
Editoval jenicek (7. 8. 2014 11:34)
- Čamo
- Člen | 798
@jenicek
Díky moc. Naozaj sa tým rozdelením metód tie ifi eliminovali a getTable() sa
použilo viac krát. Paráda. Ešte raz dík.
takže teraz to vyzerá:
namespace App\Model;
use Nette,
Nette\Security\Passwords,
Nette\Diagnostics\Debugger;
/**
* @method RegisterUser
*/
class UserModel extends BaseModel
{
/*
public function __construct(Nette\Database\Construct $db)
{
// when rewriteing parent __construct must create database connection here
// cause injection do not works then in BaseModel
parent::__construct($db);
} */
/**
* @param array $params
* @param string $table
*
* @return activeRow
*
* @throw Nette\InvalidArgumentException
* @throw Model\Exceptions\DuplicateEntry
*/
public function registerUser(Nette\Utils\ArrayHash $values)
{
$hash = Passwords::hash($values['password']);
$params = array('password' => $hash,
'username' => $values['username'],
'email' => $values['email'],
'status' => 10,
'created' => time()
);
try
{
$row = $this->insert($params);
}
catch(\PDOException $e)
{
// This catch ONLY checks duplicate entry to fields with UNIQUE KEY
$info = $e->errorInfo;
// mysql==1062 sqlite==19 postgresql==23505
if ($info[0] == 23000 && $info[1] == 1062)
{
// if/elseif returns the name of problematic field and value
if( $this->getTable()->where('username = ?', $values['username'])->fetch() )
{
$msg = 'username';
}
elseif( $this->getTable()->where('email = ?', $values['email'])->fetch() )
{
$msg = 'email';
}
throw new Exceptions\DuplicateEntryException($msg);
}
else throw $e;
}
return $row;
}
/**
* Method checks if user exists
* @param array
* @param string
*
* @return DB row or false
*/
public function signInUser($values)
{
$row = $this->getTable()->where('username = ?', $values['username'])->limit(1)->fetch();
if($row)
{
if(Passwords::verify($values['password'], $row['password']))
{
return $row;
}
}
// here MUST NOT be else!!!
return false;
}
protected function getTable()
{
return $this->database->table('users');
}
/**
* @return activeRow or false
*/
protected function insert(array $params)
{
return $this->getTable()->insert($params);
}
}
Tú kontrolu duplicít som tam ale nechal, lebo inak neviem určiť, či je to email, alebo name. Len som to okresal a vracia sa iba kľúč z $values. Presenter si už podľa toho vypíše čo chce.
PS:yetORM zatiaľ nestíham, idem na ten gitHub.
Editoval Čamo (7. 8. 2014 12:34)
- Jan Suchánek
- Člen | 404
@Čamo já bych to ještě krátil a přihlášení udělej jako má david v cd collection, má to tam super:
// jeden řádek
public function findOneBy(array $by)
{
return $this->getTable()->where($by)->fetch();
}
// víc řádků
public function findBy(array $by)
{
return $this->getTable()->where($by);
}
public function registerUser(Nette\Utils\ArrayHash $values)
{
$values->password = Passwords::hash($values->password);
$values->status = 10;
$values->created = time();
try{
// pokud nevrátí row tak je něco špatně a mělo by dojít k vyjímce!
return $this->insert($values);
} catch(\PDOException $e) {
$info = $e->errorInfo;
if($info[0] == 23000 && $info[1] == 1062){
foreach(array("username","email") as $by){
if($this->findOneBy([$by => $values->$by])){
throw new Exceptions\DuplicateEntryException($by);
}
}
}
throw $e;
}
}
}
Editoval jenicek (7. 8. 2014 18:16)