Databáze a model

Připravíme si strukturu databáze a navrhneme modelovou vrstvu. Díky Nette Database to bude opravdu rychlé a pohodlné.

Databázová struktura

Vytvoříme si databázi nazvanou quickstart a v ní tabulku uživatelů user, jejich úkolů task a seznamů úkolů list.

Image

Tabulka uživatelů user bude mít sloupce:

  • id: unikátní ID
  • username: přihlašovací jméno
  • password: hash hesla včetně soli (jako hashovací funkci použijeme Blowfish s mnoha opakováními, aby ani při použití hrubé výpočetní síly nebylo možné heslo zpětně uhádnout)
  • name: skutečné jméno uživatele, které budeme zobrazovat v aplikaci

Tabulka list bude jednoduchá:

  • id: unikátní ID
  • title: název seznamu

A nakonec tabulka úkolů task:

  • id: unikátní ID
  • text: text úkolu
  • created: čas, kdy byl úkol vytvořen
  • done: příznak, zda byl úkol splněn
  • user_id: ID uživatele, kterému je úkol přiřazen
  • list_id: ID seznamu úkolů, do kterého je úkol zařazen

Pro vytvoření databáze můžete využít Adminer, který už máte předinstalovaný na adrese http://localhost/sandbox/www/adminer/, a připravené SQL s definicemi tabulek a také ukázková data. SQL je určeno pro databázi MySQL, jelikož je nepoužívanější, nicméně s drobnými úpravami bude fungovat i s jinou databází.

Připojení k databázi

Parametry připojení k databázi uvedeme v konfiguračním souboru. Popis řetězce dsn najdete v dokumentaci PHP, zadejte i správné jméno a heslo.

database:
	dsn: 'mysql:host=localhost;dbname=quickstart'
	user:
	password:

Při editaci konfiguračního souboru si dejte pozor na odsazování. Formát NEON akceptuje odsazení tabulátorem i mezerami, ale v celém souboru musí být použito stejné odsazení. V připraveném konfiguračním souboru jsou použity tabulátory. Nette se v případě problémů ozve.

Model

Doménový model je funkční základ celé aplikace a reprezentuje problém, který aplikace řeší. Patří sem entity (což jsou objekty reprezentující úkol, seznam úkolů a uživatele), popisuje vazby mezi nimi a chování (např. jak označit úkol za splněný) atd. Je nezávislý na prezentační logice, tedy té části aplikace, která model prezentuje uživateli a zpětně překládá jeho požadavky.

Existuje řada možností, jak pojmout objektový návrh modelu a jak přistupovat k databázi. Vhodné je vytvořit třídy reprezentující jednotlivé entity a vazby mezi nimi. Přičemž perzistenci (tedy práci s databází) přenecháme samostatným třídám, tzv. data-mapperům, k čemuž lze využít třeba knihovnu Doctrine 2. Obvyklé činnosti prováděné nad entitami pak svěříme opět samostatným třídám, tzv. fasádám.

Jinou možností, kterou použijeme při tvorbě naší velmi jednoduché aplikace, je rezignovat na zapouzdření databáze a použít nástroj Nette\Database\Table, což je jakýsi chytrý „průzkumník pro databázi“. Dobereme se tak cíle mnohem rychleji a naše aplikace bude pokládat nejefektivnější SQL dotazy.

Napišme si repozitáře UserRepository, TaskRepository a ListRepository ve jmenném prostoru Todo umožňující přístup k jednotlivým entitám ze stejnojmenných tabulek. Všechny budou dědit od společného abstraktního repozitáře Repository.

Vytvoříme soubor app/model/Repository.php:

namespace Todo;
use Nette;

/**
 * Provádí operace nad databázovou tabulkou.
 */
abstract class Repository
{
	/** @var Nette\Database\Connection */
	protected $connection;

	public function __construct(Nette\Database\Connection $db)
	{
		$this->connection = $db;
	}

	/**
	 * Vrací objekt reprezentující databázovou tabulku.
	 */
	protected function getTable(): Nette\Database\Table\Selection
	{
		// název tabulky odvodíme z názvu třídy
		preg_match('#(\w+)Repository$#', get_class($this), $m);
		return $this->connection->table(lcfirst($m[1]));
	}

	/**
	 * Vrací všechny řádky z tabulky.
	 */
	public function findAll(): Nette\Database\Table\Selection
	{
		return $this->getTable();
	}

	/**
	 * Vrací řádky podle filtru, např. ['name' => 'John'].
	 */
	public function findBy(array $by): Nette\Database\Table\Selection
	{
		return $this->getTable()->where($by);
	}

}

Doporučujeme neuvádět koncovou značku PHP ?>, jazyk ji nevyžaduje a nemůže se nám stát, že by se nám za ní dostal nějaký bílý znak, díky kterému by se nám znefunkčnily stránky.

A následují soubory app/model/UserRepository.php:

namespace Todo;
use Nette;

/**
 * Tabulka user
 */
class UserRepository extends Repository
{
}

app/model/ListRepository.php:

namespace Todo;
use Nette;

/**
 * Tabulka list
 */
class ListRepository extends Repository
{
}

a app/model/TaskRepository.php:

namespace Todo;
use Nette;

/**
 * Tabulka task
 */
class TaskRepository extends Repository
{
}

A to je vše. Připravili jsme si základní kostry datového modelu.

Služby

Sekce services v konfiguračním souboru definuje takzvané služby, viz Dependency Injection. Ve výchozím konfiguračním souboru vidíme registraci 3 služeb. První z nich je authenticator, který se stará o ověřování platnosti uživatelského jména a hesla. Tuto službu využije později při implementaci přihlašování uživatelů. Zbývající 2 služby nastavují routování.

services:
	authenticator: Authenticator

	routerFactory: RouterFactory
	router: @routerFactory::createRouter

Naše třídy datového modelu, které jsme před chvílí vytvořili, zaregistrujeme právě jako služby:

services:
	authenticator: Authenticator

	routerFactory: RouterFactory
	router: @routerFactory::createRouter

	taskRepository: Todo\TaskRepository
	userRepository: Todo\UserRepository
	listRepository: Todo\ListRepository

Tím jsme zaregistrovali tři služby: taskRepository, která bude obsahovat instanci třídy Todo\TaskRepository, userRepository, která bude obsahovat instanci Todo\UserRepository a listRepository, která bude obsahovat instanci Todo\ListRepository. Všechny tři třídy vyžadují jeden parametr v konstruktoru – objekt Nette\Database\Connection zajišťující spojení s databází. Protože instance této třídy je v rámci aplikace jen jedna nemusí zde být nijak uvedena.

Pokud bychom parametry chtěli explicitně uvést, vypadala by definice takto:

services:
	authenticator: Authenticator(@nette.database.default)

	routerFactory: RouterFactory
	router: @routerFactory::createRouter

	taskRepository: Todo\TaskRepository(@nette.database.default)
	userRepository: Todo\UserRepository(@nette.database.default)
	listRepository: Todo\ListRepository(@nette.database.default)

Lepší je však zůstat u volání bez parametrů, abychom se o ně nemuseli starat. O vložení správné instance se v Nette stará takzvaný autowiring.

Nyní je vhodná chvíle upravit si definici Authenticatoru. Definice třídy Authenticator říká, že má dostat jako jediný parametr konstruktoru Nette\Database\Connection (a Nette to pozná a automaticky ho tam předá). To nám ovšem nevyhovuje, raději autentikátoru předáme instanci naší třídy starající se o uživatele: UserRepository.

// Authenticator.php

// odstraníme následující property
/** @var Nette\Database\Connection */
private $database;

// přidáme novou property pro repozitář uživatelů
/** @var Todo\UserRepository */
private $repository;

// upravíme konstruktor
public function __construct(Todo\UserRepository $repository)
{
	$this->repository = $repository;
}

Úspěšně jsme vytvořili strukturu databáze a několik jednoduchých repozitářů, které hned využjeme v presenterech..

Zdrojové kódy aplikace v této fázi naleznete na GitHubu.