Čo je to model a načo slúži?

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

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

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

mystik
Člen | 312
+
+1
-

Model ti má zapouzdřovat konkrétní chování. Model by ses neměl snažit dělat univerzální jako v tvém případě. Zjednodušeně řešeno v modelu bys měl mít volání té tvé getTable.

Čamo
Člen | 798
+
0
-

@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ť :)

Čamo
Člen | 798
+
0
-

@mystik
No ale mi to príde ako zbytočná vrstva navyše. Keď potrebujem len nejaké základné dáta, ktoré nemusím ošetrovať, len zistím, či nie sú false napr. volanie $db->table(‚authors‘). Pre toto mi to nedáva zmysel. Či je to len dobrá konvencia?

Editoval Čamo (6. 8. 2014 8:53)

mystik
Člen | 312
+
+1
-

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

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

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

@Šaman

  1. Ano ten inject nefunguje, kôli prehladnosti som zmazal konštruktor a bez neho je to blbosť
  2. To hashovanie som premiestil do modelu takže sa volá registerUser($values, $table);
  3. 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?
  4. Neviem čo je CRUD(zistím) ja mám zatiaľ len BaseModel a *Model
  5. Nerozumiem prečo nie model ale repository. Som asi hold zelenáč…

Díky

Editoval Čamo (6. 8. 2014 11:20)

Šaman
Člen | 2666
+
+1
-
  • 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
+
0
-

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

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

@Č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
+
+2
-

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

@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)

japlavaren
Člen | 404
+
0
-

nahadz to cele na github a daj sem odkaz, bude sa to lepsie pozerat ako takto

Čamo
Člen | 798
+
-4
-

Rád by som to urobil ale neviem s tým robiť. Ak existuje nejaký instantný návod, tak pošli odkaz. Už dlhšie sa na to chystám, ale ešte som sa nedokopal k tomu.

Editoval Čamo (6. 8. 2014 23:16)

BigCharlie
Člen | 283
+
0
-

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

@Č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.

Čamo
Člen | 798
+
0
-

@jenicek
Díky, ten array test dám ako type-hint. Table teda tiež dám preč. A predpokladám, že nakoniec si chcel povedať – rozdeliť na viac metód.

Jan Suchánek
Člen | 404
+
+1
-

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

@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)

Šaman
Člen | 2666
+
+1
-

Ještě bych spojil podmínky if($row && Passwords::verify($values['password'], $row['password'])), ten kaskádový dvojif vypadá divně.
A nemusíš si tam pak zdůrazňovat, že se nesmí elsovat.

Editoval Šaman (7. 8. 2014 12:39)

Čamo
Člen | 798
+
0
-

@Šaman
Jo díky. Vypadá to divne.

CZechBoY
Člen | 3608
+
0
-

Šaman napsal(a):

Ještě bych spojil podmínky if($row && Passwords::verify($values['password'], $row['password'])), ten kaskádový dvojif vypadá divně.
A nemusíš si tam pak zdůrazňovat, že se nesmí elsovat.

Ve skeletonu tam je totiž else: uživatel nenalezen.

Jan Suchánek
Člen | 404
+
0
-

@Č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)