3. díl - CMS v Nette a Doctrine 2 - Modely a Layout

PHP Nette Framework Doctrine CMS v Nette a Doctrine 2 - Modely a Layout

V minulém tutoriálu o programování CMS v Nette a Doctrine 2 jsme si založili projekt se všemi potřebnými knihovnami a úpravami. V tomto tutoriálu začneme tvořit modelovou vrstvu a přidáme layout s úvodní stránkou.

Úvodní strána v Nette Doctrine CMS

Model

Klíčem k práci s Doctrine jsou anotace. Pomocí nich si uloží do cache informace o entitách, jako jsou názvy sloupců, jejich datové typy, vazby mezi entitami atd. Pokud tedy například pomocí anotace určíme, že proměnná bude typu boolean, při výběru dat z databáze se hodnota automaticky přetypuje na TRUE nebo FALSE (v databázi hodnota bude uložena jako 1 / 0). Informace lze zapisovat i pomocí formátu XML nebo YAML, ale anotace jsou nejpoužívanější.

Entity

app/model/enti­ties/User.php

Vytvoříme si naši první jednoduchou entitu, která bude reprezentovat uživatele. Nezapomínejte na to, že Nette obsahuje třídu Nette\Security\User, jejíž objekt je ve všech presenterech a šablonách v proměnné $user. Neměli bychom tuto proměnnou přepsat, proto se proměnná naší entity bude v šablonách jmenovat $userEntity.

Pro začátek bude naše entita velmi jednoduchá. Obsahuje definici atributů, které odpovídají sloupcům v databázové tabulce user. Název je odvozen od názvu entity, proto pokud bychom měli jiný název tabulky, přidáme anotaci @ORM\Table(name="nazev_tabulky").

Stejně to platí i u názvů sloupců, tam přidáme do závorky parametr name (@ORM\Column(name="nazev_sloupce")).

<?php

namespace App\Model\Entities;

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

/**
 * Doctrine entita pro tabulku user.
 * @package App\Model\Entities
 * @ORM\Entity
 */
class User extends BaseEntity
{
        // Pomocné konstanty pro náš model.

        /** Konstanty pro uživatelské role. */
        const ROLE_USER = 1,
                ROLE_ADMIN = 2;

        /** Konstanty pro uživatelské jméno. */
        const MAX_NAME_LENGTH = 15,
                NAME_FORMAT = "^[a-zA-Z0-9]*$";

        // Proměné reprezentující jednotlivé sloupce tabulky.

        /**
         * Sloupec pro ID uživatele.
         * @ORM\Id
         * @ORM\Column(type="integer")
         * @ORM\GeneratedValue
         */
        protected $id;

        /**
         * Sloupec pro jméno.
         * @ORM\Column(type="string")
         */
        protected $name;

        /**
         * Sloupec pro heslo.
         * @ORM\Column(type="string")
         */
        protected $password;

        /**
         * Sloupec pro email.
         * @ORM\Column(type="string")
         */
        protected $email;

        /**
         * Sloupec pro IP adresu.
         * @ORM\Column(type="string")
         */
        protected $ip;

        /**
         * Sloupec pro datum registrace.
         * @ORM\Column(name="`registration_date`", type="datetime")
         */
        protected $registrationDate;

        /**
         * Sloupec role uživatele. Význam hodnot viz. konstanty pro uživatelské role.
         * @ORM\Column(type="integer")
         */
        protected $role;

        /**
         * Ověřuje, zda je uživatel v roli administrátora.
         * @return bool vrací true, pokud je uživatel administrátor; jinak vrací false
         */
        public function isAdmin()
        {
                return $this->role === self::ROLE_ADMIN;
        }
}

Pro ID je potřeba zapsat anotací více, Doctrine ho poté automaticky použije jako primární klíč.

Dále si všimněte metody isAdmin() - entita není jen přepravkou na data, ale může umět i jednoduché operace.

Všechny naše entity budou dědit od základní entity, vytvořené rozšířením Kdyby. Získá díky tomu některé příjemné funkce, např. nebudeme muset psát settery a gettery a místo toho budeme moci používat přímo $user->name.

Poznámka: Pokud atributu určíme datový typ date nebo datetime, Doctrine automaticky při tahání data z databáze vytvoří objekt třídy DateTime.

Fasády

Fasády budou služby (tzn. že v celé aplikaci bude jen jedna instance dané třídy), které budou zprostředkovávat veškeré operace.

app/model/faca­des/UserFacade­.php

Pro začátek bude opět naše třída velmi jednoduchá. Konstruktorem automaticky dostane objekt třídy Kdyby\Doctrine\EntityManager (opět malé vylepšení EntityManager od Doctrine) a přidáme metodu na vyhledání uživatele podle ID.

<?php

namespace App\Model\Facades;

use App\Model\Entities\User;
use Kdyby\Doctrine\EntityManager;
use Nette\Object;

/**
 * Fasáda pro manipulaci s uživateli.
 * @package App\Model\Facades
 */
class UserFacade extends Object
{
        /** @var EntityManager Manager pro práci s entitami. */
        private $em;

        /**
         * Konstruktor s injektovanou třídou pro práci s entitami.
         * @param EntityManager $em automaticky injektovaná třída pro práci s entitami
         */
        public function __construct(EntityManager $em)
        {
                $this->em = $em;
        }

        /**
         * Najde a vrátí uživatele podle jeho ID.
         * @param int|NULL $id ID uživatele
         * @return User|NULL vrátí entitu uživatele nebo NULL pokud uživatel nebyl nalezen
         */
        public function getUser($id)
        {
                return isset($id) ? $this->em->find(User::class, $id) : NULL;
        }
}

Metoda EntityManager::find() vyhledá entitu User podle daného ID. Normálně bychom jako první parametr měli napsat celý název třídy App\Model\Entities\User, ale magická konstanta class (zabudovaná přímo v PHP) tento název včetně jmenného prostoru obsahuje, je proto náš zápis o něco kratší.

V našem případě se buď vrátí načtená entita User (jako načtená chápejte objekt naplněný daty z databáze) nebo NULL (což vrací metoda EntityManager::find(), pokud takovou entitu nenajde).

app/config/con­fig.neon

Jelikož je UserFacade automaticky injectovaná služba, musíme ji zaregistrovat v našem konfiguračním souboru:

...
services:
        - App\Model\Facades\UserFacade
        router: App\RouterFactory::createRouter
...

Presentery

app/presenter­s/BasePresenter­.php

Entitu User i UserFacade budeme využívat v podstatě všech presenterech, proto je uložíme do našeho BasePresenteru:

<?php

namespace App\Presenters;

use App\Model\Entities\User as UserEntity;
use App\Model\Facades\UserFacade;
use Kdyby\Translation\Translator;
use Nette\Application\UI\Presenter;
use Nette\Bridges\ApplicationLatte\Template;

/**
 * Základní presenter pro všechny ostatní presentery aplikace.
 * @package App\Presenters
 */
abstract class BasePresenter extends Presenter
{
        /** @persistent null|string Určuje jazykovou verzi webu. */
        public $locale;

        /**
         * @var Translator Obstarává jazykový překlad na úrovni presenteru.
         * @inject
         */
        public $translator;

        /**
         * @var UserFacade Fasáda pro manipulaci s uživateli.
         * @inject
         */
        public $userFacade;

        /** @var UserEntity Entita pro aktuálního uživatele. */
        protected $userEntity;
…

Dále si zde přetížíme metodu startup(), kde si vytvoříme entitu User. Pokud je uživatel přihlášen, najdeme informace z databáze a uložíme je do entity. Pokud není, pak vytvoříme entitu s jedinou informací, a to že uživatel není v roli administrátora (abychom nemuseli všude připisovat podmínku $user->isLoggedIn(), ale stačilo jen $userEntity->isAdmin()).

/**
 * Volá se na začátku každé akce, každého presenteru a zajišťuje inicializaci entity uživatele.
 */
public function startup()
{
        parent::startup();
        if ($this->getUser()->isLoggedIn()) {
                $this->userEntity = $this->userFacade->getUser($this->getUser()->getId());
        } else {
                // Abychom mohli použít "$userEntity->isAdmin()", když uživatel není přihlášen.
                $entity = new UserEntity();
                $entity->role = UserEntity::ROLE_USER;
                $this->userEntity = $entity;
        }
}

Nezapomeňte do use uvést

use App\Model\Entities\User as UserEntity

Další metoda beforeRender() předává proměnné šabloně ještě před jejím vykreslením - je jedno, o kterou šablonu se jedná - předá je tedy každé šabloně, která se bude vykreslovat. Jelikož se naše entita hodí ve všech šablonách, předáme ji opět v BasePresenter.

/**
 * Volá se před vykreslením každého presenteru a předává společné proměnné do celkového layoutu webu.
 */
public function beforeRender()
{
        parent::beforeRender();
        $this->template->userEntity = $this->userEntity;
}

Šablony

app/presenter­s/templates/@la­yout.latte

Layout je hlavní šablona, která se vykresluje vždy. Šablony pro jednotlivé stránky se poté vloží makrem {include} do obsahu.

<!DOCTYPE html>
<html>
<head>
        <meta charset="utf-8">

        <title>Nette Doctrine 2 blog</title>

        <link rel="stylesheet" href="{$basePath}/css/style.css">
        <link rel="shortcut icon" href="{$basePath}/favicon.ico">
        <meta name="viewport" content="width=device-width">
        {block head}{/block}
</head>
<body>
        <div id="container">

                {if count($flashes) > 0}
                        <div id="flashes">
                                <div class="text">
                                        <div n:foreach="$flashes as $flash" n:class="flash, $flash->type">
                                                {$flash->message}
                                        </div>
                                </div>
                        </div>
                {/if}

                <div id="header">
                        <div id="logo">
                                <h1>Blog system</h1>
                        </div>
                        <div id="userInfo">
                                {if $user->isLoggedIn()}
                                        {_common.loggedAs}: {$userEntity->name}
                                {/if}
                        </div>
                        <div id="menu">
                                <a n:href="Homepage:default">{_menu.homepage}</a>
                        </div>
                </div>
                <div id="content">
                        {include content}
                </div>
                <div id="footer">
                        <div class="text">
                                &copy; Konesoft Corporation
                        </div>
                </div>
        </div>

        {block scripts}
        <script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
        <script src="//nette.github.io/resources/js/netteForms.min.js"></script>
        <script src="{$basePath}/js/main.js"></script>
        {/block}
</body>
</html>

Nette kromě proměnné $user předává šablonám automaticky i proměnné $flashes, která obsahuje pole flash zpráv (ty se vytvoří metodou Nette\Application\UI\Control::flashMessage($message, $type)), a $basePath - ta obsahuje název kořenové složky projektu.

Všimněte si, že texty v naší šabloně jsou připravené pro překlad. K tomuto článku přiložím všechny soubory pro překlad, ať to nemusíme v každém článku dopisovat. Je zde podpora pro český a anglický jazyk.

Také bude přiložen CSS soubor. Přijde mi bezdůvodné ho tu ukazovat a vysvětlovat (je ke stažení v projektu), design není cílem této série. Stejně si ho upravíte dle vlastního vkusu.

Poznámka: U překladu znamená první slovo název souboru, ostatní jsou názvy klíčů. Proto makro {_menu.homepage} použije klíč homepage ze souboru menu (přesný soubor se ještě určí podle jazyka). Soubory jsou ve formátu NEON.

app/presenter­s/templates/Ho­mepage/default­.latte

Na úvodní stránce bude zobrazeno několik nejnovějších článků. To si vytvoříme až později, nyní přidáme jen stránku s nadpisem, aby Nette nevyhazovalo chybu 404.

{block content}
<h2>{_homepage.header}</h2>

To je ze třetího článku vše. Příště přidáme možnost registrace uživatele.


 

Stáhnout

Staženo 77x (3.43 MB)
Aplikace je včetně zdrojových kódů v jazyce PHP

 

  Aktivity (2)

Článek pro vás napsal Martin Konečný (pavelco1998)
Avatar
Autor se o IT moc nezajímá, raději by se věnoval speciálním jednotkám jako jsou SEALs nebo SAS. Když už to ale musí být něco z IT, tak tvorba web. aplikací v PHP.

Jak se ti líbí článek?
Celkem (1 hlasů) :
55555


 



 

 

Komentáře
Zobrazit starší komentáře (5)

Avatar
Martin Konečný (pavelco1998):

ahoj,
díky těmto magickým funkcím se nám v podstatě jen zjednoduší psaní - pokud by sis udělal explicitně sette/getter, pak by např. $user->name = "Jméno" zavolalo $user->setName("Jméno").
Proto settery/gettery není nutné v tomto případě psát, pokud nám stačí vzít/uložit surovou hodnotu. Samozřejmě pokud bychom chtěli dělat nějakou validaci vstupu, pak je potřeba set metodu napsat.

Jedná se tedy jen o zkrácení zápisu, abys neměl hromadu funkcí, které buď jen něco uloží nebo vrátí (klasicky viz příklady u Symfony a Doctrine). Pokud chceš mít jistotu, že nepůjde použít nějaký atribut, který má být doslova privátní (tzn. nemít na něj ani getter), pak zřejmě bude vhodnější od základní Kdyby\BaseEntity nedědit.

Poznámka: v novější verzi Kdyby jsou tyto funkce v traitě Kdyby\Doctrine\En­tities\MagicAc­cessors, BaseEntity je zavrhnuta.

 
Odpovědět 25. října 22:27
Avatar
rurijs
Člen
Avatar
rurijs:

Ahoj,
dělal jsem vše podle návodu, no píše mi to:

User Deprecated
Nette\DI\State­ment::setEnti­ty is deprecated, change Statement object itself.

Nevíte co s tím?

 
Odpovědět 6. listopadu 21:48
Avatar
Odpovídá na rurijs
Martin Konečný (pavelco1998):

Ahoj,
můžeš přiložit screen z Laděnky?

 
Odpovědět 6. listopadu 22:06
Avatar
rurijs
Člen
Avatar
Odpovídá na Martin Konečný (pavelco1998)
rurijs:

Už jsem to vyřešil (vzal jsem si composer z tohohle projektu, není tam totiž aktuální nette, atd), takže downgrade verzí.. a hláška zmizela.

 
Odpovědět 6. listopadu 22:22
Avatar
Odpovídá na Martin Konečný (pavelco1998)
Dominik Gavrecký:

Prečo si constructor nevytvoril v class-e BaseFacades, ktorou by si následne rozšíril každú Facade ?

/**
         * Sloupec pro datum registrace.
         * @ORM\Column(name="`registration_date`", type="datetime")
         */
        protected $registrationDate;

Prečo neurčuješ CURRENT_TIMESTAMP ?

Odpovědět 15. listopadu 19:51
Hlupák nie je ten kto niečo nevie, hlupákom sa stávaš v momente keď sa na to bojíš opýtať.
Avatar
Odpovídá na Dominik Gavrecký
Martin Konečný (pavelco1998):

Protože Base* třídy jsou takový antipattern. Dědičnost se má použít v případě, že její potomci opravdu využijí to, co ta základní třída obsahuje (atributy, chování (metody)). Tady pak mohou nastat dvě nepříjemné situace:

  1. Některá z fasád nevyužije to, co BaseFacade nabízí - pak dědičnost logicky nedává smysl
  2. Některá z fasád by nedědila od BaseFacade - tady bys zase měl podivně strukturu aplikace

Typicky se do Base* tříd ve vrstvě "Model" dává objekt, který zprostředkovává komunikaci s databází - EntityManager, Nette\Database\Con­nection, PDO atd.
Když bys ale měl fasádu, která by s databází nekomunikovala, k čemu by měla v sobě ten objekt mít (pokud by dědila od BaseFacade)? To z hlediska návrhu OOP nedává smysl.
Navíc se ty fasády v podstatě stanou závislými na té BaseFacade. Pokud budu chtít upravit něco v BaseFacade, automaticky se mi to promítne do všech fasád v aplikaci. Někdy to může být fajn, ale někdy si tím naopak pěkně nabiju hubu :D
Raději budu mít fasády nezávislé, ať se případné změny týkají jednotlivě každé fasády. Dívám se na to tak, že každá fasáda je nezávislá část aplikace, která (dle SRP) se stará o jednu konkrétní věc - a k tomu nemusí vždy využít např. připojení k databázi (nebo cokoliv jiného, co definuje BaseFacade).

Dědičnost se má použít tam, kde to má smysl, ne abych si ušetřil psaní.

Co se týká CURRENT_TIMESTAMP - to by se zřejmě dalo. Já si jen zvykl s daty pracovat ručně :) ono se to dá udělat i tak, že si to datum vytvoříš v konstruktoru:

class User
{

        protected $registrationDate;


        public functon __construct()
        {
                $this->registrationDate = new \DateTime();
        }

}

Magie je v tom, že když entitu vytváří Doctrine, když data vytahuje z databáze, pak se konstruktor nevolá, a tedy se do toho neuloží žádná jiná hodnota - jen ta, kterou tam Doctrine nastrčila.
Vytvoření objektu bez zavolání konstruktoru jde přes reflexi:

$rc = new ReflectionClass("User");
$entity = $rc->newInstanceWithoutConstructor();

viz http://php.net/…structor.php.

Editováno 15. listopadu 20:09
 
Odpovědět  +1 15. listopadu 20:08
Avatar
Odpovídá na Martin Konečný (pavelco1998)
Dominik Gavrecký:

Nesúhlasím s tebou ale je to znova vec pohody a myslenia programátora ... Čo v prípade že programátor si spraví niečo takéto ?

<?php
/**
 * Created by PhpStorm.
 * User: Dominik Gavrecký
 * Date: 15.11.2016
 * Time: 19:45
 */

namespace App\Model\Facades;

use Kdyby\Doctrine\EntityManager;
use Nette\Object;

/**
 * Class BaseFacades
 * @package app\model\Facades
 */
class BaseFacade extends Object
{
    const
        ENTITIE = '';

    /**
     * @var EntityManager
     */
    private $em;

    /**
     * BaseFacades constructor.
     * @param EntityManager $em
     */
    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function getAll()
    {
        return $this->em->getRepository($this::ENTITIE)->findAll();
    }


    public function delete($id)
    {
        $query = $this->em->getRepository($this::ENTITIE)->find($id);
        return $this->em->remove($query);
    }

    ...
}

Dosť ti to uľahčí prácu nie ?

A k tomu CURENT_TIMESTAMP

/**
     * @ORM\Column(type="datetime", options={"comment":"Dátum a čas registrácie uživateľa", "default": 0})
     */
    protected $time;

Dá sa to riešiť aj takto :)

Odpovědět 16. listopadu 17:59
Hlupák nie je ten kto niečo nevie, hlupákom sa stávaš v momente keď sa na to bojíš opýtať.
Avatar
Odpovídá na Dominik Gavrecký
Martin Konečný (pavelco1998):

A co v případě, že budu mít fasádu, která nebude pracovat s databází, a tedy nebude potřebovat EntityManager a metody getAll() a delete()? To pak nebude dědit od BaseFacade, nebo bude mít nesmyslně metody a závislosti, které vůbec nepotřebuje? To pak půjdeš proti logice a budeš to mít návrhově špatně :)

Navíc jedna fasáda obvykle nepracuje jen s jednou entitou, takže v tmo $this::ENTITIE nevidím moc reálné využití.

 
Odpovědět 16. listopadu 19:31
Avatar
Odpovídá na Martin Konečný (pavelco1998)
Dominik Gavrecký:

Niekde som čítal ze v Nette by mal model (fasada) pracovať s jednou tabuľkou. Teda toho sa aj držím ... A co v prípade ze všetky fasády s ňou pracovať budú ? A o takýchto "co ak" sa tu môžme baviť do nekonečna

Odpovědět 16. listopadu 20:02
Hlupák nie je ten kto niečo nevie, hlupákom sa stávaš v momente keď sa na to bojíš opýtať.
Avatar
Odpovídá na Dominik Gavrecký
Martin Konečný (pavelco1998):

Jedna fasáda ani jedna entita nemusí pracovat jen s jednou tabulkou v databázi. Hlavně z pohledu ORM bys ani neměl řešit, že nějakou relační databázi máš (a tedy nějaké tabulky), měl bys uvažovat čistě v objektech (ale je jasné, že to úplně spolehlivě nejde).
Fasády mají zastřešovat určitou operaci, ve které naopak často bývá zapojeno více než jedna entita / tabulka. Minimálně bys narazil na problém, kdy bys měl jednu entitu složenou z více tabulek.

Pokud jsi si jistý, že všechny fasády s EM pracovat budou, pak je to zřejmě v pořádku. Nedá se ale brát tu logiku OOP vždy doslova, někdy se pro ulehčení práce musí udělat nějaká výjimka, která nějaké pravidlo porušuje. Já jen říkám, jak by to asi mělo být (aspoň dle mého názoru, co jsem za těch pár let zkušeností získal), ne že to tak být musí. I třeba magie jako Kdyby\Doctrine\En­tities\MagicAc­cessors není z hlediska logiky správná, ale kdo by pořád psal ty gettery a settery... někdy se pro zjednodušení práce nějaké pravidlo poruší, jen je potřeba na to dávat pozor, aby se to později v projektu nevymstilo.

 
Odpovědět 16. listopadu 20:18
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 15. Zobrazit vše