Databáze a entita stránek

Doctrine 2

Tak dnes si vyrobíme databázi a v ní pak tabulku, do které budeme ukládat stránky. Vyrobit databázi snad budeš umět. Já pro správu databází používám bezva balík adminer. Jestli neznáš, doporučuju vyzkoušet. Čili vyrob novou databázi, dej jí název podle svého uvážení, třeba rsrs jako já. Není rozumné používat uživatele root pro všechny databáze, takže vyrob i uživatele, který bude mít práva jen na právě vyrobenou databázi. Nicméně rozhodnutí nechám na tobě - na lokále to zas tak velkou roli nehraje. Jen to podporuje špatné zvyky.

Důležité - uprav soubor config/local.neon tak, aby obsahoval správné přihlašovací údaje k právě vytvořené databázi.

Při práci s databází budeme používat Doctrine2. Její výhodou je mimo jiné i to, že si sama dokáže udržovat pořádek v tabulkách a polích. Výuka Doctrine2 je zase jiné téma, takže pokud nemáš zkušenosti a chceš vědět něco víc, najdi si nějaký pěkný článek na netu. Necítím se být povolán vysvětlovat něco, co se sám ještě učím.

Je zapotřebí odpoutat se od tradičního pojetí databáze, kdy bereme řádek jako záznam. V Doctrine2 budeme záznamy mapovat na Entity. Samotnému mi to ještě automaticky moc nejde v hlavě přehodit :-)

Entita Page

Začneme tím, že si vyrobíme entitu stránky. Pro ukládání záznamů do tabulky používám metodu traverzování okolo stromu. Sice se o něco hůř ukládá či maže, ale řazení a výběr je velmi snadné. Pro tuto techniku je zapotřebí popsat každý záznam jako uzel, který má levý index a pravý index. Pokud stránka neobsahuje žádné podstránky, má pravý index hodnotu o jedna vyšší než levý. Opět se moc nechci pouštět do teorie ukládání pomocí traverzování okolo stromu.  Moc hezký článek je na https://php.vrana.cz/traverzovani-kolem-stromu-prakticky.php.

Ve složce "src" vytvoř složku PageModule. V ní pak složku Model a v ní zase složku Entity. V Entity pak soubor Page.php. Nejprve popíšu váznam jednotlivých polí, které chceme mít v databázi v tabulce Page.

  • id - to je jasné. Jde o identifikátor. Mělo by to být automatické kladné číslo
  • parent - identifikátor rodiče, nadřazené stránky
  • lft - číslo na levé straně uzlu
  • rgt - číslo na pravé straně uzlu
  • level - to asi není úplně nutné, ale jde o úroveň vnoření. Bude vždy o jedna vyšší, než má rodič.
  • status - stránka může mít status 1 = zveřejněná, či 0 = rozepsaná, či nezveřejněná
  • in_menu - menu, ve kterém se má stránka zobrazit
  • on_homepage - příznak toho, že stránka se má zobrazit jako domovská
  • name - název stránky
  • title - titulek v prohlížeči
  • description - popisek do meta description
  • menu_title - titulek v menu
  • url - URL adresa stránky
  • perex - krátký souhrn článku - prostě perex
  • text - plný text článku

Podle libosti samozřejmě přidávej či ubírej pole tak, jak potřebuješ. Můžeš doplnit třeba pole pro náhledový obrázek a jeho alt popis, OG značky pro sdílení na sociálních sítích, pole pro URL kanonické stránky, případně pole pro meta robots atd.

Entitu musíme definovat pomocí komentářů se speciálním významem. Tak na začátku tedy definujeme vlastnosti celé tabulky, jako název a indexy:

<?php

namespace PageModule\Model\Entity;

use Kdyby;
use Doctrine\ORM\Mapping as ORM;

/**
 * Page
 *
 * @ORM\Table(name="page",
 *     uniqueConstraints={@ORM\UniqueConstraint(name="url", columns={"url"})},
 *     indexes={
 *      @ORM\Index(name="parent_idx",  columns={"parent"}),
 *      @ORM\Index(name="lft_idx", columns={"lft"}),
 *      @ORM\Index(name="rgt_idx", columns={"rgt"}),
 *      @ORM\Index(name="level_idx", columns={"level"}),
 *      @ORM\Index(name="status_idx", columns={"status"}),
 *      @ORM\Index(name="name_perex_text_title", columns={"name", "perex", "text", "title"}, flags={"fulltext"})
 *     })
 * @ORM\Entity
 */
class Page
{

Následuje definice všech polí v tabulce:

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer", nullable=false, options={"unsigned":true})
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var integer
     *
     * @ORM\Column(type="integer", options={"unsigned":true})
     */
    private $parent;

    /**
     * @var integer
     *
     * @ORM\Column(type="integer", options={"unsigned":true})
     */
    private $lft;

    /**
     * @var integer
     *
     * @ORM\Column(type="integer", options={"unsigned":true})
     */
    private $rgt;

    /**
     * @var integer
     *
     * @ORM\Column(type="integer", options={"unsigned":true})
     */
    private $level;

    /**
     * @var boolean
     *
     * @ORM\Column(type="boolean")
     */
    private $status;

    /**
     * @var string
     *
     * @ORM\Column(name="in_menu", type="string", length=150, nullable=true)
     */
    private $inMenu;

    /**
     * @var boolean
     *
     * @ORM\Column(type="boolean")
     */
    private $onHomepage;

    /**
     * @var string
     *
     * @ORM\Column(name="title", type="string", length=80, nullable=true)
     */
    private $title;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=80, nullable=false)
     */
    private $name;

    /**
     * @var string
     *
     * @ORM\Column(name="menu_title", type="string", length=80, nullable=true)
     */
    private $menuTitle;

    /**
     * @var string
     *
     * @ORM\Column(name="url", type="string", length=255, nullable=false)
     */
    private $url;

    /**
     * @var string
     *
     * @ORM\Column(name="description", type="string", length=255, nullable=true)
     */
    private $description;

    /**
     * @var string
     *
     * @ORM\Column(name="perex", type="text", length=65535, nullable=true)
     */
    private $perex;

    /**
     * @var string
     *
     * @ORM\Column(name="text", type="text", nullable=true)
     */
    private $text;

Ty divné věci za zavináčem jsou důležité. Řídí vlastnosti polí. Některá jsou integer, jiná text, mají různou velikost, či povolenou hodnotu null apod. Nech je beze změn, pokud nevíš přesně, co by se stalo při změně.

Každá vlastnost by měla mít metody getter a setter pro nastavení a přečtení hodnot:

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @param int $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return int
     */
    public function getParent()
    {
        return $this->parent;
    }

    /**
     * @param int $parent
     */
    public function setParent($parent)
    {
        $this->parent = $parent;
    }

Další gettery a settery si vyrob už sám. Případně pokud máš nějaké pokročilejší IDE, tak možná bude umět všechno vygenerovat za tebe.

Jakmile to budeš mít, spusť si příkazový řádek ve složce www. Zadej příkaz

php index.php

Měl bys vidět výpis příkazů. Zvol si a zadej příkaz:

php index.php orm:schema-tool:create

Vygeneruje se tabulka Page, připravená ukládat záznamy. Vlož ručně jeden záznam. Doplň si:

  • lft = 1
  • rgt = 2
  • parent = 0
  • url = /
  • status = 0
  • level = 0
  • name = 'Home'
  • on_homepage = 1

Zbytek doplň podle svého - title, description, text atd.

Model pro práci se záznamy

Dále budeme potřebovat model pro práci se záznamy - čtení, vkládání, aktualizaci a mazání. Vím, že by se model měl psát ve více vrstvách - facade, repository, services atd., ale já to mám zatím jen ve fasádě. Vytvoř proto ve složce src/PageModule/Model složku Facade. Uvnitř ní pak soubor PageFacade.php. Začátek souboru:

<?php

namespace PageModule\Model\Facade;

use Kdyby\Doctrine\EntityRepository;
use PageModule\Model\Entity\Page;
use Kdyby\Doctrine\EntityManager;
use Kdyby\Doctrine\InvalidStateException;

final class PageFacade
{
    /** @var EntityManager $em */
    private $em;

    /** @var EntityRepository $repository */
    private $repository;

    /**
     * PageFacade constructor.
     *
     * @param EntityManager $em
     */
    public function __construct(EntityManager $em)
    {
        $this->em = $em;
        $this->repository = $em->getRepository(Page::class);
    }

Nastavíme namespace, předáme závislost na EntityManageru a v konstruktoru nastavíme proměnnou $this->repository.

Vytvořme si teď metodu pro výběr všech entit. Každý repositář obsahuje metodu findAll(), takže by mohlo stačit ji jen zavolat. Nicméně protože používáme to traverzování okolo stromu, potřebujeme ještě nastavit řazení podle hodnoty lft. Takže přidej metodu:

    /**
     * @return array|mixed
     */
    public function findAll()
    {
        return $this->repository->findBy([], ['lft' => 'ASC']);
    }

Jednoduchá metoda pro nalezení entity podle id:

    /**
     * @param int $id
     *
     * @return mixed|null|Page
     */
    public function find($id)
    {
        return $this->repository->find($id);
    }

Další metoda má za úkol uložení změn v entitě:

    /**
     * Method save
     *
     * @param \PageModule\Model\Entity\Page $item
     *
     * @throws \Exception
     */
    public function save(Page $item)
    {
        if ($item->getId() === null) {
            throw new InvalidStateException('Záznam neexistuje');
        }
        try {
            $this->em->flush($item);
        } catch (\Exception $exc) {
            throw new \Exception;
        }
    }

Metody pro vložení či smazání nejsou v případě traverzování okolo stromu tak jednoduché. Budu se jim tedy věnovat zvlášť v další kapitole.

Můžeme ale přidat další metody, třeba pro nalezení entity podle URL:

    /**
     * @param string $url
     *
     * @return mixed|null|Page
     */
    public function findByUrl($url)
    {
        return $this->repository->findOneBy(['url' => $url]);
    }

Doplnit další metody pro výběr podle jednotlivých podmínek by bylo také jednoduché. Nicméně toto lze simulovat jedinou metodou, která by přijímala jeden argument, což by bylo pole s podmínkou. Čili místo výše uvedené metody findByUrl a dalších variant, by mohla existovat pouze jedna metoda:

    /**
     * @param array $cond
     *
     * @return mixed|null|Page
     */
    public function findOneBy(array $cond)
    {
        return $this->repository->findOneBy($cond);
    }

V případě, že bychom chtěli najít více záznamů, stačilo by opět doplnit jen metodu, která by volala $this->repository->findBy($cond, $orderBy); To už nechám na tobě. Abychom mohli pracovat s třídou PageFacade, musíme s ní nějak "seznámit" systém. Vyrobíme tedy rozšíření, stejně jako jsme ho vytvářeli u HomePage modulu.

Ve složce src/PageModule tedy vyrob složku DI. Uvnitř ní vyrob soubor services.neon s obsahem:

services:
    - PageModule\Model\Facade\PageFacade

No a samotný doplněk bude ve stejné složce jako soubor PageExtension.php. Začátek souboru:

<?php

namespace PageModule\DI;

use Nette;
use BaseModule\DI\BaseExtension;
use Kdyby\Doctrine\DI\IEntityProvider;

final class PageExtension extends BaseExtension implements IEntityProvider
{
    /**
     * Method getEntityMappings
     *
     * @return array
     */
    public function getEntityMappings()
    {
        return [
            'Page' => __DIR__ . '/../Model/Entity/',
        ];
    }

Cílem metody je namapovat entitu Page. Vyrobili jsme si konfiguraci v services.neon, tak ji nahrajeme a doplníme routy pro budoucí presenter, kerý bude zobrazovat stránku:

    /**
     * Method loadConfiguration
     */
    public function loadConfiguration()
    {
        $builder = $this->getContainerBuilder();

        $this->compiler->loadDefinitions(
            $builder,
            $this->loadFromFile(__DIR__ . '/services.neon')['services'],
            $this->name
        );
        $this->appendRoute('Page:Front', '/<url>', 'Page:view');
    }

A když už jsme si namapovali entitu, potřebujeme namapovat i presenter. Doplň tedy metodu:

    /**
     * Method beforeCompile
     *
     * @throws \Nette\DI\ServiceCreationException
     */
    public function beforeCompile()
    {
        $builder = $this->getContainerBuilder();
        $builder->getDefinition($builder->getByType(Nette\Application\IPresenterFactory::class))->addSetup(
            'setMapping',
            [
                ['Page' => 'PageModule\*Module\Presenters\*Presenter'],
            ]
        );
    }

A nakonec ještě musíme nahrát rozšíření. Stačí do konfiguračního souboru common.neon přidat řádek s rozšířením. Přidej ho pod BaseExtension:

    page: PageModule\DI\PageExtension

Opět máš možnost stáhnout si aplikaci po dnešní kapitole. Jen nebudu zbytečně nahrávat třeba složku vendor, ve které se nic nezměnilo.

Celý seriál o vývoji redakčního systému

Komentáře

O mně

Jmenuji se Rudolf Svátek - lektor výpočetní techniky, trochu PHP programátor a SEO konzultant na volné noze.

Adresa

Příčná 326/3
736 01 Havířov

Kontakty

Email: office@rudolfsvatek.cz
Telefon: +420 777 828 353
Skype: svatekr