Tento projekt vznikl pro předmět WA na PEF MENDELU. Tento průvodce ukáže základní použití mikroframeworku Slim pro vytvoření aplikace, která zhruba odpovídá části zadání z předmětu APV (tedy evidence osob a jejich adres).
Proti plnotučným frameworkům se Slim na první pohled jeví jako nedotažený projekt, ale to je záměr. Tento framework řeší v podstatě jen tzv. routing, předání dat ze vstupu do skriptu a řádnou HTTP odpověď. Jeho hlavní výhoda je snadné použití a možnost namíchat si vlastní oblíbené knihovny. Nejlépe jej použijete jako backend pro REST rozhraní, kde ani není potřeba řešit šablony a generování formulářů.
Tento návod je už nepřesný a slévají se zde různé přístupy. Nejlepší bude použít nový návod popsaný v knize The Making of Web Application. Tento návod může dobře posloužit jako dokumentace myšlenkového pochodu, který vedl ke kostře aplikace použité v uvedené knize, kterou najdete na BitBucketu.
- Slim 3.7.x - dokumentace
- Apache 2.x a PHP 7.x
- NetBeans (volitelné)
- nainstalovat Composer tak, aby šel spouštět příkazem
composer
z příkazového řádku - volitelně i NodeJS a balíčkovací systém npm (také by mělo jít spustit přes příkazový řádek)
- volitelně i Git
- vytvořte si složku pro projekt
- stažení frameworku pomocí Composeru, použít příkaz
composer create-project slim/slim-skeleton .
(vč. tečky na konci - tzn. do aktuálního adresáře). Složka, kam projekt vytváříte musí být prázdná. - nyní je dobré založit projekt v NetBeans nebo jiném IDE
- na lokálním webovém serveru zkontrolovat, že aplikace běží (otevřít složku http://locahost/slim_demo/public, mělo by se zobrazit jméno frameworku s odkazem na stránky - welcome obrazovka).
Může být nutné nastavit direktivu RewriteBase
v souboru /public/.htaccess
na aktuální cestu k aplikaci
např. RewriteBase /~user/aplikace/public
.
Důležité adresáře a soubory:
/public
- veřejná část aplikace, to co je opravdu přístupné přes HTTPindex.php
- vstupní soubor
/src
- vlastní zdrojové kódy aplikacedependencies.php
- globální závislosti, např. připojení na databázimiddleware.php
- zde můžete nadefinovat tzv. middlewareroutes.php
- zde bude implementována vlastní aplikacesettings.php
- nastavení aplikace, která můžete libovolně rozšířit
/templates
- základní šablony v čistém PHP, nahradíme šablonami Latte
Slim jako takový nepodporuje žádné ORM, ani nemá vlastní DB vrstvu, je nutné tedy databázi připojit např. pomocí PDO knihovny z PHP.
Nejprve je nutné nastavit připojení k databázi v souboru /src/settings.php
nebo lépe v souboru /.env
(abychom měli
oddělenou konfiguraci pro různá prostředí):
Soubor /src/settings.php
načítá nastavení z /.env
. Pokud nechcete /.env
používat, vyplňte hodnoty přímo
a pokračujte k vytvoření instance PDO.
...
'database' => [
'host' => getenv('DB_HOST'),
'user' => getenv('DB_USER'),
'pass' => getenv('DB_PASS'),
'name' => getenv('DB_NAME'),
]
...
Soubor /.env
obsahuje lokální nastavení a je v Gitu ignorován (tzn. je zahrnut v souboru /.gitignore
):
DB_HOST=localhost
DB_USER=user
DB_PASS=pass
DB_NAME=wa_slim
Aby nám soubor /.env
fungoval a nastavení z něj se načetly, musíme stáhnout rozšíření pro načítání jeho obsahu:
composer require vlucas/phpdotenv
. Potom je nutné načíst tyto proměnné, toto můžeme provést hned na začátku
sobuoru /src/settings.php
:
$dotenv = new Dotenv\Dotenv(__DIR__);
$dotenv->load();
Nakonec zaregistrujeme PDO do dependency containeru naší
aplikace v /src/dependencies.php
. Tyto závislosti lze potom snadno získat v obsluhách rout.
$container['db'] = function ($c) {
$settings = $c->get('settings')['database'];
$pdo = new PDO("mysql:host=" . $settings['host'] . ";dbname=" . $settings['name'], $settings['user'], $settings['pass']);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->query("SET NAMES 'utf8'");
return $pdo;
};
Databázi můžete naimportovat ze souboru /db_struktura.sql
.
UPDATE 2018 Pro knihovnu Latte jsme pro vás vytvořili už nachystaný adaptér na adrese https://github.com/ujpef/latte-view. Postupujte podle návodu v dokumentaci této knihovny a tuto sekci přeskočte, popisuje vytvoření něčeho velmi podobného, nicméně popisuje i autloading přes Composer, který je potřeba např. pro modelovou vrstvu.
Slim opět nemá žádný šablonovací systém, ve výchozím stavu podporuje šablony v čistém PHP, což je nevyhovující i pro
malé projekty. Je tedy nutné stáhnout pomocí Composeru např. knihovnu Latte: composer require latte/latte
.
Pro zobrazení šablon je nutné napsat malý adaptér a podobně jako v případě databáze je dobré zaregistrovat Latte jako
službu. Adaptér můžeme umístit do složky /classes
, kterou vytvoříme.
<?php
use \Psr\Http\Message\Response as Response;
class LatteView {
private $latte;
private $pathToTemplates;
function __construct(Latte\Engine $latte, $pathToTemplates) {
$this->latte = $latte;
$this->pathToTemplates = $pathToTemplates;
}
function render(Response $response, $name, array $params = []) {
$name = $this->pathToTemplates . '/' . $name;
$output = $this->latte->renderToString($name, $params);
$response->getBody()->write($output);
return $response;
}
}
Tento adaptér potom zaregistrujeme opět do dependency containeru aplikace v /src/dependencies.php
, všimněte si, že
jsme ze souboru /src/settings.php
převzali nakonfigurovanou cestku k šablonám. Cestu k cache jsme nastavili "natvrdo",
ale může samozřejmě být také v settings - tuto složku je nutné vytvořit, přidat do /.gitignore
a nastavit do ní právo
zápisu všem uživatelům (např. příkazem chmod 0777 cache
nebo přes WinSCP/FileZillu). Cache pro šablony je volitelná.
Samozřejmě je nutné původní $container['renderer']
s PHP šablonami smazat.
$container['renderer'] = function($c) {
$settings = $c->get('settings')['renderer'];
$engine = new Latte\Engine();
$engine->setTempDirectory(__DIR__ . '/../cache');
$latteView = new LatteView($engine, $settings['template_path']);
return $latteView;
};
Abychom nemuseli soubory ze složky /classes
includovat ručně, je možné využít konfiguraci composeru
v souboru
/composer.json
a nastavit nahrávání souborů ze složky /classes
ve stylu PSR-0:
```json` ... "autoload" : { "psr-0": { "": "classes/" } }, ...
### Výpis osob
Nyní máme funkční šablony, vyzkoušíme tedy předání nějakých dat z DB přímo do šablony, v souboru `/src/routes.php`
načteme osoby a pošleme je prostřednictvím adaptéru do Latte šablony. Proměnná `$app` vzniká v souboru `/public/index.php` a je
to jakýsi kontejner celé aplikace. Aplikace je sestavena z obsluh různých HTTP metod a URL adres - jde tedy o
[routing s parametry](https://www.slimframework.com/docs/objects/router.html). Je možné vytvořit i routu na libovolnou
HTTP metodu pomocí `$app->any('/route', function(...) {...});` když je jedno, kterou HTTP metodu prohlížeč použije.
```php
$app->get('/', function ($request, $response, $args) {
$stmt = $this->db->query("SELECT * FROM persons ORDER BY last_name");
$persons = $stmt->fetchAll();
return $this->renderer->render($response, 'index.latte', [
'persons' => $persons
]);
});
V šabloně je samozřejmě přístupná proměnná $persons
, která je naplněna daty z DB (pokud žádná nemáte, zkuste ručně vložit
nějaké řádky do tabulky persons
. Komunikace s databází by samozřejmě měla být v try {} catch() {}
bloku.
Všimněte si, že každá obsluha routy získává objekt popisující vstup
a výstup a argumenty (proměnné z URL). Aby nám mohlo IDE
pomáhat, je dobré přidat k argumentům funkce i typ, který mají mít: Psr\Http\Message\Request
a
Psr\Http\Message\Response
, nejlépe pomocí use
:
use Psr\Http\Message\Request;
use Psr\Http\Message\Response;
$app->get('/', function (Request $request, Response $response, $args) {
...
});
Do rout pridame jednu GET a jednu POST akci. Pro vykreslení formuláře nic spciálního nepotřebujeme:
$app->get('/pridat', function(Request $request, Response $response, $args) {
return $this->renderer->render($response, 'create.latte');
});
Formulář jako takový je vytvořen v HTML. Přidání osoby je klasciké vložení dat přes PDO, zajímavý je postup
získání dat z těla POST požadavku - používá se metoda $request->getParsedBody()
, která na základě nastavené
HTTP hlavičky převede obsah na asociativní pole. Po vložení dat do databáze je dobré přesměrovat návštěvníka
na výpis osob, což se provede přidáním hlavičky Location do HTTP odpovědi.
$app->post('/ulozit', function(Request $request, Response $response, $args) {
try {
$data = $request->getParsedBody();
$stmt = $this->db->prepare('INSERT INTO persons (first_name, last_name, nickname) VALUES (:fn, :ln, :nn)');
$stmt->bindValue(':fn', $data['first_name']);
$stmt->bindValue(':ln', $data['last_name']);
$stmt->bindValue(':nn', $data['nickname']);
$stmt->execute();
return $response->withHeader('Location', 'vypis');
} catch (PDOException $e) {
if($e->getCode() == 23000) {
return $this->renderer->render($response, 'create.latte', ["duplicate" => true]);
} else {
die($e->getMessage());
}
}
});
Náš formuář zatím neřeší, zda byla vyplněny všechny povinné údaje (spoléhá na atribut required
), ani neřeší předání
vyplněných dat do šablony v případě chyby. Nicméně je ošetřen chybový stav při vložení duplicitních dat a to
znovuzobrazením prízdného formuláře. URL vypis
je nastavena jako volitelný text (v hranatých závorkách) ve
výchozí routě pro výpis osob:
$app->get('/[vypis]', function (...) {...});
Do formuláře pro vytvoření osoby je nutné přidat roletku s adresami, rozšíříme tedy routu pro zobrazení formuláře o výběr dat adres z DB. Tento výběr raději realizujeme jako funkci, protože ji budeme potřebovat i v případě znovuzobrazení formuláře při pokusu o vložení duplicitního záznamu.
function loadLocations(PDO $db) {
try {
$stmt = $db->query('SELECT * FROM locations ORDER BY city');
return $stmt->fetchAll();
} catch(PDOException $e) {
die($e->getMessage());
}
}
$app->get('/pridat', function(Request $request, Response $response, $args) {
return $this->renderer->render($response, 'create.latte', [
'locations' => loadLocations($this->db)
]);
});
Data do formuláře si můžeme předat pomocí vnořeného asoc. pole:
$app->get('/pridat', function(Request $request, Response $response, $args) {
return $this->renderer->render($response, 'create.latte', [
'form' => [
'first_name' => '',
'last_name' => '',
'nickname' => '',
'id_location' => ''
],
'locations' => loadLocations($this->db)
]);
});
V případě, že je formulář odeslán chybně/duplicitně vyplněn, můžeme tyto data snadno přepsat polem s hodnotami z těla HTTP POST požadavku a můžeme i přidat vysvětlující hlášku:
$app->post('/ulozit', function(Request $request, Response $response, $args) {
$data = $request->getParsedBody();
$hlaska = '';
if (!empty($data['first_name']) && !empty($data['last_name']) && !empty($data['nickname'])) {
...
$hlaska = 'Takovato osoba jiz existuje';
...
} else {
$hlaska = 'Nebyly vyplneny vsechny povinne informace';
}
return $this->renderer->render($response, 'create.latte', [
'hlaska' => $hlaska,
'form' => $data,
'locations' => loadLocations($this->db)
]);
});
Alternativně je možné proměnnou $data
uložit do $_SESSION
a přesměrovat na routu /pridat
, kde zařídíme
volitelné předání dat ze $_SESSION
do dat formuláře.
Mazání osob provedeme přidáním formuláře s potvrzením na výpisu osob:
<form action="smazat/{$p['id']}" onsubmit="return confirm('Opravdu smazat osobu?')" method="post">
<input type="submit" value="Smazat" />
</form>
ID osoby pro smazání bude přímo v URL a bude podle toho vypadat i routa, po smazání přesměrujeme na výpis osob. Je dobré
si všimnout jak je postavena URL pro přesměrování - jelikož se o přesměrování stará webový prohlížeč na základě HTTP
hlavičky, a ten si myslí, že jsme ve složce /smazat
, je nutné z této složky přejít o úroveň nahoru a zde otevřít
routu vypis
, proto ../vypis
.
$app->post('/smazat/{id}', function(Request $request, Response $response, $args) {
try {
$stmt = $this->db->prepare('DELETE FROM persons WHERE id = :id');
$stmt->bindValue(':id', $args['id']);
$stmt->execute();
return $response->withHeader('Location', '../vypis');
} catch (PDOException $e) {
die($e->getMessage());
}
});
Editace a vytvoření nové osoby bude velice podobné. Problém je, že jakmile nasměrujeme prohlížeč na URL editace/{id}
,
začne si myslet, že všechny relativní cesty vedou od složky editace
, např. odkaz v menu <a href="vypis">...</a>
povede na neexistující adresu editace/vypis
. Proto je nutné zavést <base href="...">
značku v <head>
. Pomocí
této značky budou všechny relativní adresy (začínající jinak než http://
a /
) prefixovány hodnotou v href
atributu
<base>
značky. Takže pokud máte aplikaci nahranou např. ve složce slim_demo
, měla by vaše <base>
značka vypadat
takto: <base href="/slim_demo/public/">
.
Vhodné místo k detekci aktuální cesty k aplikaci je middleware, který se spouští vždy před/po vlastní obsluze routy. Je také nutné přidat do našeho adaptéru pro Latte možnost vložit proměnnou do každé renderované šablony. Middleware je i vhodné místo pro ověření, zda je uživatel přihlášen nebo ne.
$app->add(function (Request $request, Response $response, callable $next) {
$currentPath = dirname($_SERVER['PHP_SELF']);
$this->view->addParams([
'base_path' => $currentPath == '/' ? $currentPath : $currentPath . '/'
]);
return $next($request, $response);
});
Bootstrap přidáme z CDN, podobně i jeho závislost na jQuery. Jako obvykle je potřeba
přidat třídu form-control
na vstupní pole formulářů a zabalit celou strukturu aplikace do prvku s třídou container
.
Přihlašování vyžaduje obvykle vytvoření tabulky s uživateli v databázi, zde si ukážeme jen základní přihlášení a ověření
pomocí middleware a uživatelský účet uložíme staticky do src/settings.php
(login i heslo je "admin"):
'auth' => [
'user' => 'admin',
'pass' => 'd033e22ae348aeb5660fc2140aec35850c4da997'
],
Obvykle se očekává, že v aplikaci bude několik rout, které budou přístupné pouze pro přihlášené, je proto dobré tyto
routy seskupit a navázat na ně middleware, který
bude ověřovat přihlášení v session (tedy bude aktivován jen pro tuto skupinu). První parametr metody group()
je
URL prefix pro všechny routy uvnitř, v callbacku potom definujeme jednotlivé routy na $this
(místo $app
):
$app->group('/user', function () {
$this->get('/profil', function(Request $request, Response $response) {});
$this->get('/odhlasit', function(Request $request, Response $response) {});
})->add(function(Request $request, Response $response, callable $next) {
if(!empty($_SESSION['logged_in'])) {
$this->renderer->addParams(['logged_in' => true]);
return $next($request, $response);
} else {
return $response->withStatus(401)->withHeader('Location', '../vypis');
}
});
Pomocí metody addParams
na třídě LatteView
můžeme do šablony poslat informaci o tom, že je uživatel přihlášen a tím
např. zařídit skrytí přihlašovacího tlačítka v menu.
Samotné přihlášení je obyčejný POST požadavek, který musí být veřejný a jen ověří uživatele podle loginu a hesla. Tomuto ještě předchází vykreslení přihlašovacího formuláře. Úspěšné přihlášení je zaznamenáno do session:
$app->get('/prihlasit', function(Request $request, Response $response) {
return $this->renderer->render($response, 'login.latte');
});
$app->post('/prihlasit', function(Request $request, Response $response) {
$data = $request->getParsedBody();
if($data['login'] == $this->settings['auth']['user'] && sha1($data['pass']) == $this->settings['auth']['pass']) {
$_SESSION['logged_in'] = true;
return $response->withHeader('Location', 'user/profil');
}
return $response->withHeader('Location', 'prihlasit');
});
Na routy chráněné pomocí middleware se dá dostat jen po přihlášení, jinak je uživatel přesměrován jinam.
REST rozhraní je to, pro co byl framework Slim primárně navržen. Zkusíme vytvořit pomocí jQuery jednoduchý skript,
který bude AJAXem tahat data z backendu. Konkrétně půjde o načtení detailu osoby do popup okna. Knihovna jQuery už
je v projektu připojena kvůli Bootstrapu, není nutné ji tedy přidávat. Do výpisu osob přidáme prvek s atributem
data-person-info
, krom toho, že jej použijeme k předání ID osoby, bude sloužit i k vyvolání vlastního popupu.
Jmenovaný atribut použijeme jako CSS selektor.
<span class="glyphicon glyphicon-info-sign" data-person-info="{$['id_person']}"></span>
Ve složce public
vytvoříme např. složku js
a do ní vložíme soubor person_detail.js
. Tento potom připojíme
buď v hlavičce v souboru templates/layout.latte
nebo někde v souboru s výpisem osob templates/index.latte
:
<script type="text/javascript" src="js/person_detail.js"></script>
Pro prvotní ověření jen obsloužíme událost click na element s data atributem v souboru person_detail.js
:
$(document).ready(function() {
$('[data-person-info]').click(function() {
alert(this.dataset.personInfo);
});
});
V backendu nachystáme API endpoint pro zjištění informací o osobě podle ID. Routa vypadá podobně jako ostatní, ale
odpověď není generována přes Latte adaptér, ale pomocí metody withJSON
, která vezme asociativní pole a převede jej
na JSON strukturu.
$app->get('/api/osoba/{id}', function (Request $request, Response $response, $args) {
try {
$stmt = $this->db->prepare('SELECT * FROM persons WHERE id = :id');
$stmt->bindValue(':id', $args['id']);
$stmt->execute();
$person = $stmt->fetch(PDO::FETCH_ASSOC);
if($person) {
return $response->withJSON($person);
} else {
return $response->withJSON(['message' => 'Person not found.'], 404);
}
} catch (PDOException $e) {
return $response->withJSON(['message' => $e->getMessage()], 500);
}
});
Na frontendu z této URL stáhneme data pomocí funkce getAJAX()
z jQuery.
$('[data-person-info]').click(function() {
$.getJSON('api/osoba/' + this.dataset.personInfo, function(response) {
console.log(response);
alert(response.first_name + ' ' + response.last_name + '\r\n' + response.nickname);
});
});
Sledujte v konzoli a síťové konzoli vývojářských nástrojů, co se děje.
V dokumentaci wrapperu Latte View je popsáno, jak vytvořit makro pro šablony
{link routeName}
, které poslouží pro generování cest v HTML šablonách.
$app->get('/neco', function(Request $request, Response $response, $args) {
//...
})->setName('routeName');
<a href="{link routeName}">Klikem se vyvolá routa /neco</a>
Makro generuje pouze URL (eventualně umí vložit parametry místo placeholderů), ale query parametry už přidáváte ručně.
$app->get('/neco', function(Request $request, Response $response, $args) {
$id = $request->getQueryParam('id');
//...
})->setName('routeName');
<a href="{link routeName}?id={$id}">Klikem se vyvolá routa /neco?id=123</a>
Místo proměnné base path lze použít {link index}
.
$app->get('/', function(Request $request, Response $response, $args) {
//...
})->setName('index');
<head>
<base href="{link index}">
</head>
Případně lze base značku vypustit a generovat cesty ke statickým souborům přes toto makro.
<head>
<script type="text/javascript" src="{link index}js/person_detail.js"></script>
<!-- nebo -->
<script type="text/javascript" src="{$base_path}js/person_detail.js"></script>
</head>
Je vidět, že aplikace se poměrně rychle rozrůstá, proto by nebylo špatné, rozdělit routy do více souborů (pomocí funkce
include()
nebo require()
).
Příkazem git clone http://adresa.repositare.cz/nazev.git slozka
se vám stáhne z Gitu kopie projektu. Jelikož jsou
některé důležité soubory a složky nastavené v souboru .gitignore
, je potřeba primárně spustit příkaz
composer install
, aby se stáhl vlastní framework a jeho knihovny. Poté nastavit konfigurace v /.env
, který
vytvoříte jako kopii souboru .env.example
.