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í:
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í 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
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:
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.
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.
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 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.