Administrace systému - první kroky

Je na čase začít se zajímat o administrační část. Tu oddělíme do modulu Admin. Část pro uživatele webu pak dáme do modulu Front. Po dlouhém vybírání a testování, jsem si vybral několik knihoven a rozšíření, které mi administraci usnadňují:

  • AdminLTE - systém šablon pro administraci. Je volně ke stažení, má celkem dobrou dokumentaci a dobře vypadá. Teď vidím, že před pár dny už vydali verzi 3. Sice zatím beta, ale pohlídám si to a podívám se na změny
  • Bootstrap 4 - celkem známá klasika, která velmi usnadňuje hlavně stylování
  • jQuery - výborná javascriptová knihovna, řešící elegantně spousty rutinních úkolů
  • Ublaboo Datagrid - velmi dobře zpracovaný datagrid pro Nette. Rozumí si i s Doctrine 2, takže super
  • TinyMCE - webový editor se spoustou doplňků a pluginů, umožňující tvořit obsah skoro jako Word

Kromě těchto "větších" balíků používám ještě pár menších knihoven pro nějaké jednorázovky, ale to už si doplníš cokoli bys potřeboval. Já třeba kvůli zobrazení formátovaného kódu PHP používám knihovnu prism.js a další.

Stažení knihoven

Stažení všech knihoven se dá zařídit najednou pomocí nástroje Bower. Funguje podobně jako Composer. Do složky vendor nahraj soubor bower.json s následujícím obsahem:

{
  "name": "rsrs",
  "authors": [
    "svatek <rudolf@svatkovi.net>"
  ],
  "description": "rsrs",
  "main": "rsrs",
  "license": "MIT",
  "homepage": "",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ],
  "dependencies": {
    "jquery": "^3.4.1",
    "admin-lte": "^2.4.12",
    "bootstrap4": "bootstrap4#^4.3.1",
    "smartmenus": "^1.1.0",
    "raphael": "^2.2.8",
    "moment": "^2.24.0",
    "nprogress": "^0.2.0",
    "popper.js": "^1.15.0",
    "tooltip.js": "https://unpkg.com/tooltip.js",
    "nette.ajax.js": "^2.3.0",
    "nette-forms": "^3.0.0",
    "jquery.cookie": "^1.4.1",
    "jquery-ui-sortable": "jquery-ui-sortable#*",
    "bootstrap-checkbox": "^1.5.0",
    "happy": "^1.0.7",
    "ublaboo-datagrid": "^6.1.1",
    "tinymce": "^5.0.7",
    "prism": "^1.16.0"
  }
}

Aby to fungovalo, musíš si samozřejmě Bower nainstalovat, ale to není předmětem tohoto seriálu. Nicméně pokud ho máš nainstalovaný, zadej

bower install

Stáhnou se všechny knihovny a nahrají se do složky bower_component.

Pro hladký a rychlý běh webu je celkem rozhodující rychlost načítání. A tam platí, že čím méně zdrojů musíš načítat, tím lépe. Doporučuje se tedy minifikovat a spojit všechny CSS do jednoho, stejně jako JS. K tomu používám knihovnu WebLoader od Honzy Marka. Tady použijeme k instalaci opět Composer. Ještě ho naučíme minifikovat výsledný zdroj. Zadej

composer require janmarek/webloader
composer require matthiasmullie/minify
composer require ublaboo/datagrid 5.7

Base presenter

Máme v plánu vyrobit časem několik modulů. Kromě stránek třeba uživatele, nastavení celé aplikace atd. Vyrobíme tedy nějaký BasePresenter, ze kterého budou ostatní moduly dědit. V příští kapitole to ještě rozvedu. Na začátku bude jednoduchý - vyrob složku src/BaseModule/AdminModule/Presenters a v ní soubor BasePresenter.php. Měl by mít obsah:

<?php

namespace BaseModule\AdminModule\Presenters;

use Nette;

abstract class BasePresenter extends Nette\Application\UI\Presenter
{

    /** @var Nette\Caching\Cache */
    protected $cache;

    public function __construct()
    {
        parent::__construct();
    }

    protected function startup()
    {
        parent::startup();

        $storage = new Nette\Caching\Storages\FileStorage(__DIR__ . '/../../../../temp/cache');
        $this->cache = new Nette\Caching\Cache($storage);
    }

    /**
     * @return Nette\Security\User
     */
    public function getUser()
    {
        $user = parent::getUser();
        $user->getStorage()->setNamespace('Admin');

        return $user;
    }

    /**
     * @return mixed
     */
    public function formatLayoutTemplateFiles()
    {
        $originalLayoutFiles = parent::formatLayoutTemplateFiles();
        $layoutFiles[] = __DIR__ . '/templates/@layout.latte';
        $layoutFiles = array_merge($layoutFiles, $originalLayoutFiles);

        return $layoutFiles;
    }
}

Tady jsou zajímavé 3 věci:

  1. v metodě startup() si nastavuji cache
  2. v metodě getUser() si nastavuji prostor Admin. Používám na to tu cache
  3. v metodě formatLayoutTemplateFiles říkám, kde leží @layout.latte pro administraci

Každý modul také bude pravděpodobně mít nějaký výpis položek, čili grid. Společné věci opět můžeme mít v nějakém BaseGridu. Čili opět zakládáme složku - src/BaseModule/AdminModule/Components/Grid a v ní soubor BaseGrid.php s obsahem:

<?php

namespace BaseModule\AdminModule\Components;

use Nette\Application\UI\Control;
use Ublaboo\DataGrid\Localization\SimpleTranslator;

abstract class BaseGrid extends Control
{
    /**
     * @var SimpleTranslator
     */
    protected $gridTranslator;

    /**
     * BaseGrid constructor.
     */
    public function __construct()
    {
        parent::__construct();
        $this->gridTranslator = new SimpleTranslator(
            [
                'ublaboo_datagrid.no_item_found_reset' => 'Žádné položky nenalezeny. Filtr můžete vynulovat',
                'ublaboo_datagrid.no_item_found' => 'Žádné položky nenalezeny.',
                'ublaboo_datagrid.here' => 'zde',
                'ublaboo_datagrid.items' => 'Položky',
                'ublaboo_datagrid.all' => 'všechny',
                'ublaboo_datagrid.from' => 'z',
                'ublaboo_datagrid.reset_filter' => 'Resetovat filtr',
                'ublaboo_datagrid.group_actions' => 'Hromadné akce',
                'ublaboo_datagrid.show_all_columns' => 'Zobrazit všechny sloupce',
                'ublaboo_datagrid.hide_column' => 'Skrýt sloupec',
                'ublaboo_datagrid.action' => 'Akce',
                'ublaboo_datagrid.previous' => 'Předchozí',
                'ublaboo_datagrid.next' => 'Další',
                'ublaboo_datagrid.choose' => 'Vyberte',
                'ublaboo_datagrid.execute' => 'Provést',
                'ublaboo_datagrid.save' => 'Uložit',
                'ublaboo_datagrid.cancel' => 'Zrušit',
                'ublaboo_datagrid.add' => 'Nový',
                'ublaboo_datagrid.edit' => 'Upravit',
                'ublaboo_datagrid.show_default_columns' => 'Zobrazit výchozí sloupce',
                'ublaboo_datagrid.show_filter' => 'Zobrazit filtr',
                'ublaboo_datagrid.per_page_submit' => '',
                'Inserted' => 'Vloženo',
            ]
        );
    }
}

Vlastně slouží zatím jen jako překlad Gridu do češtiny, abychom to nemuseli dělat v každém jednotlivém Gridu.

Šablona layoutu

Administrace bude mít svůj layout, který bude vycházet ze šablony AdminLTE. Najdi si tedy soubor vendor/bower_components/admin-lte/index.html a překopíruj ho do složky src/BaseModule/AdminModule/Presenters/templates. Pak ho přejmenuj na @layout.latte.

Samozřejmě, že tak, jak je, ho nemůžeme nechat. Za prvé se odkazuje na css a js zdroje, na které teď nevidí, když jsme ho přesunuli a za druhé neumí zobrazovat nic dynamického, ale jen ukázku z toho Admin LTE.

Takže nejprve celou složku bower_components překopíruj do složky www. Je to sice hromada souborů, ale neboj - časem si uklidíme. Teď jen dočasně, abychom si předvedli, že to bude fungovat. Pak upravíš cestu ke zdrojům css a js. V souboru @template.latte uprav všechny cesty, které začínají "bower_" na "/bower_". Například řádky:

    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css">
    <link rel="stylesheet" href="bower_components/Ionicons/css/ionicons.min.css">

změň na:

    <link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.min.css">
    <link rel="stylesheet" href="/bower_components/Ionicons/css/ionicons.min.css">

Všimni si, že jsou tam i řádky, které začínají na "dist/". Těm musíš dodat celou tu cestu, takže např. řádek

<link rel="stylesheet" href="dist/css/AdminLTE.min.css">

změň na

<link rel="stylesheet" href="/bower_components/admin-lte/dist/css/AdminLTE.min.css">

AdminLTE nepoužívá Ublaboo, ale my ho chceme. Čili doplníme jeho css a js:

<link rel="stylesheet" href="/bower_components/ublaboo-datagrid/assets/datagrid.css">
<link rel="stylesheet" href="/bower_components/ublaboo-datagrid/assets/datagrid-spinners.css">
<script src="/bower_components/ublaboo-datagrid/assets/datagrid.js"></script>
<script src="/bower_components/ublaboo-datagrid/assets/datagrid-instant-url-refresh.js"></script>
<script src="/bower_components/ublaboo-datagrid/assets/datagrid-spinners.js"></script>

Samozřejmě, že css přidáváš v head za ostatní a js úplně dole těsně nad </body>.

Prozatím poslední úprava bude náhrada obsahu v divu Content Wrapper. Nahraď vše, co patří do <div class="content-wrapper"> za následující obsah:

    <!-- Content Wrapper. Contains page content -->
    <div class="content-wrapper">
        <!-- Content Header (Page header) -->
        {snippetArea content}
            {include #content}
        {/snippetArea}
        <!-- /.content -->
    </div>
    <!-- /.content-wrapper -->

Prozatím by to nefungovalo a hodilo by to chybu, ale to hned napravíme, protože vyrobíme komponentu pro výpis stránek a zobrazíme si ji v presenteru.

Page komponenta Grid a presenter

Začneme tím, že si vytvoříme komponentu pro grid. Každou komponentu tvořím tak, že potřebuje minimálně 3 soubory.

  • interface továrny
  • samotná komponenta jako továrna
  • šablona komponenty

Interface pak zaregistruji jako službu a celé to jede tak nějak magicky samo. Na vysvětlování komponent jsou tu povolanější. Já neznám přesně ani terminologii, natož abych tě zasvěcoval do problematiky. Jsem asi takový Nette programátor jako v reálném životě řidič - vím kam nalít benzín a olej, jak se nastartuje a jak se mám rozjet, ale že bych uměl vysvětlit jak maká spojka a převodovka, to ne :-) Umím prostě auto něka odřídit, stejně jako vyrobit si komponentu v Nette tak, aby dělala co chci. Jestli to ale jde lépe, to nejsem schopen posoudit.

Tak nejprve rozhraní. Bude ve složce PageModule/AdminModule/Components/Page/Grid. Soubor s názvem IPageGridFactory.php bude obsahovat pouze:

<?php

namespace PageModule\AdminModule\Components\Page\Grid;

interface IPageGridFactory
{
    /**
     * @return PageGrid
     */
    public function create();
}

Tu anotaci @return tam určitě nech. Systém podle ní pozná s čím bude pracovat. Teď zatím komponenta PageGrid neexistuje, takže ji vytvoříme. Ve stejné složce vyrob soubor PageGrid.php:

<?php

namespace PageModule\AdminModule\Components\Page\Grid;

use BaseModule\AdminModule\Components\BaseGrid;
use PageModule\Model\Facade\PageFacade;
use Ublaboo\DataGrid\DataGrid;

class PageGrid extends BaseGrid
{

    /**
     * @var \Kdyby\Doctrine\QueryBuilder
     */
    private $items;

    /**
     * @var string
     */
    private $templateFile = __DIR__ . '/Page.grid.latte';

    /**
     * @var PageFacade
     */
    private $facade;

    /**
     * PagesGrid constructor.
     *
     * @param PageFacade $facade
     *
     * @throws \Kdyby\Doctrine\NotSupportedException
     */
    public function __construct(PageFacade $facade)
    {
        parent::__construct();
        $this->facade = $facade;
        $this->items = $this->facade->findAll();
    }

    /**
     * Method render
     */
    public function render()
    {
        $this->template->setFile($this->templateFile);
        $this->template->render();
    }

    /**
     * @param $name
     * @return DataGrid
     * @throws \Ublaboo\DataGrid\Exception\DataGridColumnStatusException
     */
    protected function createComponentGrid($name)
    {
        /** @var DataGrid */
        $grid = new DataGrid($this, $name);
        $grid->setTranslator($this->gridTranslator);

        $grid->setDataSource($this->items);

        $grid->addColumnText('name', 'Název')->addAttributes(['style' => 'width: 40%;'])->setFilterText('name');

        $grid->addColumnText('url', 'Url')->setFilterText('url');

        $grid->addColumnStatus('status', 'Status')
            ->addAttributes(['style' => 'width: 10%;'])
            ->addOption(1, 'Aktivní')
            ->setClass('btn-success ajax')
            ->endOption()
            ->addOption(0, 'Neaktivní')
            ->setClass('btn-danger ajax')
            ->endOption();

        return $grid;
    }
}

Na začátku to nebude umět nic, než jen vypsat stránky. Sice se to bude na oko tvářit, že je možné změnit stav z aktivní na neaktivní a opačně, ale zatím to fungovat nebude. Ale neplakej - to doděláme příště.

Když si všimneš, v komponentě nastavuji $templateFile na název Page.grid.latte. Takže to je ten třetí soubor ve složce. Může mít jednoduchý obsah:

{snippet grid}
    {control grid}
{/snippet}

Nicméně zde se fantazii meze nekladou a pokud chceš, můžeš si šablonu různě upravit, aby zobrazovala další informace. Třeba takto:

<div class="row" id="snippet--grid">
    <div class="col-sm-12" id="snippet-grid-grid">
        <div class="panel panel-default datagrid datagrid-grid">
            <div class="panel-body">
                <div class="box box-success">
                    <div class="box-header with-border">
                        <h3 class="box-title">Stránky</h3>
                    </div>
                    {snippet grid}
                        {control grid}
                    {/snippet}
                </div>
            </div>
        </div>
    </div>
</div>

Nesmíme zapomenout na registraci služeb v src/PageModule/DI/services.neon. Ten bude teď vypadat takto:

services:
    - PageModule\Model\Facade\PageFacade
    - PageModule\AdminModule\Components\Page\Grid\IPageGridFactory

Navíc zatím nemáme definovanou routu pro administraci stránek. Čili přidáme řádek do PageExtension.php, který teď bude vypadat takto:

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

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

Na řadě je presenter, který vyrobí komponentu a zobrazí ji v administraci. Čili vyrob ve složce src/PageModule/AminModule/Presenters/Page soubor PagePresenter.php a dej mu obsah:

<?php

namespace PageModule\AdminModule\Presenters;

use BaseModule\AdminModule\Presenters\BasePresenter;
use PageModule\AdminModule\Components\Page\Grid\IPageGridFactory;
use PageModule\AdminModule\Components\Page\Grid\PageGrid;

class PagePresenter extends BasePresenter
{

    /** @var IPageGridFactory @inject */
    public $gridFactory;

    protected function startup()
    {
        parent::startup();
    }

    /**
     * @return PageGrid
     */
    protected function createComponentGrid()
    {
        return $this->gridFactory->create();
    }
}

Jeho šablona ve složce Presenters/templates bude mít obsah:

{block content}
    {snippet grid}
        {control grid}
    {/snippet}
{/block}

Pro dnešek vážně poslední věc, je vytvoření souboru .htacces, který zajistí přesměrování. Musí ležet ve složce www a jeho obsah:

# Apache configuration file (see https://httpd.apache.org/docs/current/mod/quickreference.html)
Require all granted

# disable directory listing
<IfModule mod_autoindex.c>
	Options -Indexes
</IfModule>

# enable cool URL
<IfModule mod_rewrite.c>
	RewriteEngine On
	# RewriteBase /

	# use HTTPS
	# RewriteCond %{HTTPS} !on
	# RewriteRule .? https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

	# prevents files starting with dot to be viewed by browser
	RewriteRule /\.|^\.(?!well-known/) - [F]

	# front controller
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteRule !\.(pdf|js|ico|gif|jpg|jpeg|png|webp|svg|css|rar|zip|7z|tar\.gz|map|eot|ttf|otf|woff|woff2)$ index.php [L]
</IfModule>

# enable gzip compression
<IfModule mod_deflate.c>
	<IfModule mod_filter.c>
		AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript application/json application/xml image/svg+xml
	</IfModule>
</IfModule>

No a teď chvilka napětí - zkus zadat do prohlížeče http://rsrs.loc/admin. Jestli jsem někde něco nevynechal, pak bys měl vidět výpis stránek, který tedy zatím obsahuje jen stránku Home, ale už se něco děje :-)

Kdyby se ale objevila nějaká chyba, můžeš si opět stáhnout celou aplikaci. Dnes jí nabídnu opravdu celou, protože jsme toho změnili hodně. Chce to trochu trpělivosti - je to 50 Mb. Máme tam totiž tu složku s bower 2x. Časem to uklidíme.

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