Lekce 6 - Jednoduchý redakční systém v Symfony - Model článků
V minulé lekci, Jednoduchý redakční systém v Symfony - Struktura projektu, jsme si připravili projektovou strukturu pro jednoduchý redakční systém.
Dnes se vrhneme na třídu a repozitář.
Databáze
V první řadě se podíváme na nastavení i obsah databáze. V Symfony se většinou pro práci s databází používá knihovna třetích stran Doctrine. Ta pracuje s principem Objektově Relačního Mapování (ORM), jehož základ tvoří entity. S jednou takovou entitou jsme se již setkali v lekci kurzu o tvorbě operace kalkulačky.
src/Entity/Article.php
Nyní si obdobně vytvoříme entitu reprezentující článek v našem redakčním systému, následně ji obohatíme o anotace z Doctrine knihovny a necháme si podle ní vygenerovat celou relační databázovou strukturu. Doctrine potom zajistí, že položky z naší databáze v dané tabulce se automaticky namapují na instance oné entitní třídy (tedy na objekty), což je právě princip celého ORM.
Třídu si můžeme nechat vygenerovat příkazem:
php bin/console make:entity
Název entity bude Article
. Poté se konzole ptá, jaké
vlastnosti (atributy) bude entita mít, můžete si to sami nastavit podle
entity níže (atribut $id
je vygenerován vždy automaticky a má
ho každá vygenerovaná entita). Každopádně se nám vygeneruje automaticky i
repozitář, takže to ušetří práci. Entitu tedy budeme mít v
následující podobě:
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; /** * Reprezentuje záznamy databázové tabulky článků v redakčním systému. * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository") * @UniqueEntity("url", message="Článek s touto URL adresou již existuje!") * @package App\Entity */ class Article { /** * @var int Unikátní ID článku. * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * @var string Titulek článku. * @ORM\Column(type="string") * @Assert\NotBlank(message="Titulek článku nemůže být prázdný!") */ private $title; /** * @var string Text (obsah) článku. * @ORM\Column(type="text") * @Assert\NotBlank(message="Obsah článku nemůže být prázdný!") */ private $content; /** * @var string Unikátní URL adresa článku. * @ORM\Column(type="string", unique=true) * @Assert\NotBlank(message="URL adresa článku nemůže být prázdná!") */ private $url; /** * @var string Krátký popis článku. * @ORM\Column(type="string") * @Assert\NotBlank(message="Popis článku nemůže být prázdný!") */ private $description; /** * Getter pro ID článku. * @return int ID článku */ public function getId(): int { return $this->id; } /** * Getter pro titulek článku. * @return null|string titulek článku */ public function getTitle(): ?string { return $this->title; } /** * Setter pro titulek článku. * @param string|null $title titulek článku * @return Article sebe */ public function setTitle(string $title = null): self { $this->title = $title; return $this; } /** * Getter pro obsah článku. * @return null|string obsah článku */ public function getContent(): ?string { return $this->content; } /** * Setter pro obsah článku. * @param string|null $content obsah článku * @return Article sebe */ public function setContent(string $content = null): self { $this->content = $content; return $this; } /** * Getter pro URL adresu článku. * @return null|string URL adresa článku */ public function getUrl(): ?string { return $this->url; } /** * Setter pro URL adresu článku. * @param string|null $url URL adresa článku * @return Article sebe */ public function setUrl(string $url = null): self { $this->url = $url; return $this; } /** * Getter pro popis článku. * @return null|string popis článku */ public function getDescription(): ?string { return $this->description; } /** * Setter pro popis článku. * @param string|null $description popis článku * @return Article sebe */ public function setDescription(string $description = null): self { $this->description = $description; return $this; } }
Všimněte si zde hlavně nových ORM anotací, které "transformují" onu třídu na příslušnou databázovou tabulku a její atributy na sloupce oné tabulky s daným typem a vlastnostmi.
Za zmínku také stojí anotace @UniqueEntity
, která v
kombinaci s unikátní URL článku zajistí, že taková entita se vyskytne
pouze jednou. To se hodí, když budeme například pomocí formuláře
vytvářet nové.
Když již zmiňujeme formuláře, dále si můžete povšimnout, že jsme
zde rovnou připravili i anotace, které využijeme při tvorbě formuláře pro
práci s články, který tím pádem můžeme také vytvořit na základě
této entity
Nastavení přístupových údajů a vytvoření databáze
Abychom si nyní mohli nechat vygenerovat databázovou strukturu, musíme
ještě nastavit přístupové údaje do naší databáze. To uděláme tak, že
upravíme hodnotu proměnné DATABASE_URL
uvnitř souboru
.env
v kořenové složce:
... DATABASE_URL=mysql://<uživatelské jméno>:<heslo>@localhost:3306/<název databáze>?serverVersion=5.7 ...
Databázi poté nemusíme vytvářet ručně, ale pro její vytvoření rovnou použít Doctrine příkaz:
php bin/console doctrine:database:create
Generování databázové struktury
Pro vygenerování naší relační databázové struktury v podobě jedné tabulky článků opět použijeme Doctrine příkazy.
Nejdříve vytvoříme novou tzv. migraci pomocí příkazu:
php bin/console make:migration
Tento příkaz vytvoří PHP skript ve složce src/Migrations/
,
kde zafixuje aktuální databázovou strukturu. To je výhodné zejména pokud
bychom ji do budoucna dále měnili, jelikož další migrační skripty mohou
vycházet z předchozí verze a pouze aplikovat změny.
Následujícím příkazem poté aplikujeme všechny migrační změny:
php bin/console doctrine:migrations:migrate
Může se nás to zeptat, jestli chceme migraci určitě provést (můžou se
ztratit nějaké data či měnit schéma). Jelikož databázi máme úplně
novou, nemáme se ani o co bát a stačí migraci odsouhlasit pomocí slovíčka
yes
a potvrzení enterem.
Pokud jsme vše udělali dobře, měli bychom v databázi vidět novou
tabulku article
a doctrine_migration_versions
. Druhá
tabulka obsahuje informace o migracích a tabulku bychom neměli nijak měnit
či mazat.
Pokud si chcete vypsat všechny dostupné Doctrine příkazy a
jejich popis, použijte příkaz php bin/console list doctrine
Předpřipravené články
My si do naší tabulky článků doplníme ještě dva předpřipravené články, jeden pro úvodní stránku a jeden pro vypsání chyby. To bývá standardem u většiny redakčních systémů.
Podle oficiálního postupu bych si pro tyto články měl připravit tzv. Doctrine Fixture. To by ale vyžadovalo instalaci externího bundlu DoctrineFixturesBundle a toho vás chci prozatím ušetřit. Vyřešil jsem to tedy pouhým SQL skriptem, který stačí spustit nad naší databází:
INSERT INTO `article` VALUES ('1', 'Úvod', '<p>Vítejte na našem webu!</p>\r\n\r\n<p>Tento web je postaven na <strong>jednoduchém redakčním systému v Symfony frameworku</strong>. Toto je úvodní článek, načtený z databáze.</p>', 'uvod', 'Úvodní článek na webu v Symfony v PHP'); INSERT INTO `article` VALUES ('2', 'Stránka nebyla nalezena', '<p>Litujeme, ale požadovaná stránka nebyla nalezena. Zkontrolujte prosím URL adresu.</p>', 'chyba', 'Stránka nebyla nalezena.');
Model
Přesuneme se k vytváření modelu. Třídám v modelové vrstvě, které mají pracovat s Doctrine entitami, se říká repositáře. To je trochu rozdíl oproti modelu kalkulačky, ale v podstatě je pouze kosmetický.
src/Repository/ArticleRepository.php
Vytvořme si tedy nyní repositář pro správu článků:
<?php namespace App\Repository; use App\Entity\Article; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\ORMException; use Doctrine\Persistence\ManagerRegistry; /** * Repositář pro správu článků v redakčním systému. * @method Article|null find($id, $lockMode = null, $lockVersion = null) * @method Article|null findOneBy(array $criteria, array $orderBy = null) * @method Article[] findAll() * @method Article[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @package App\Repository */ class ArticleRepository extends ServiceEntityRepository { /** @inheritdoc */ public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Article::class); } /** * Vrátí článek z databáze podle jeho URL. * @param string $url URl článku * @return Article|null první článek, který má danou URL nebo null pokud takový neexistuje */ public function findOneByUrl(string $url): ?Article { return $this->findOneBy(['url' => $url]); } /** * Uloží článek do systému. * Pokud není nastaveno ID, vloží nový, jinak provede editaci. * @param Article $article článek * @throws ORMException Jestliže nastane chyba při ukládání článku. */ public function save(Article $article): void { $this->getEntityManager()->persist($article); $this->getEntityManager()->flush($article); } /** * Odstraní článek s danou URL. * @param string $url URL článku * @throws ORMException Jestliže nastane chyba při mazání článku. */ public function removeByUrl(string $url): void { if ($article = $this->findOneByUrl($url)) { $this->getEntityManager()->remove($article); $this->getEntityManager()->flush(); } } }
Pokud jsme si nechali generovat třídu entity článku, budeme mít vygenerovanou i základní kostru jejího repositáře, kterou nyní pouze upravíme.
Důležité je zmínit, že každý repositář by měl dědit ze třídy
ServiceEntityRepository
, která mu umožňuje pracovat s entitami,
vybírat je a ukládat i mazat je z databáze. Každá instance třídy
Article
pak reprezentuje jeden záznam v databázi.
Co stojí za povšimnutí je, že ukládání nebo mazání entity bychom
měli ještě potvrdit voláním flush()
. Do té doby se totiž
fyzicky neprovede SQL příkaz nad databází.
To je z dnešní lekce vše.
Příště, v lekci Jednoduchý redakční systém v Symfony - Výpis článku, se budeme věnovat kontrolerům a šablonám.
Měl jsi s čímkoli problém? Zdrojový kód vzorové aplikace je ke stažení každých pár lekcí. Zatím pokračuj dál, a pak si svou aplikaci porovnej se vzorem a snadno oprav.