diff --git a/application/cs/@home.texy b/application/cs/@home.texy index 833406fcbd..9bbdba7d6b 100644 --- a/application/cs/@home.texy +++ b/application/cs/@home.texy @@ -2,19 +2,7 @@ Nette Application ***************** .[perex] -Balíček `nette/application` představuje základ pro tvorbu interaktivních webových aplikací. - -- [Jak fungují aplikace? |how-it-works] -- [Bootstrap] -- [Presentery |presenters] -- [Šablony |templates] -- [Moduly |modules] -- [Routování |routing] -- [Vytváření odkazů URL |creating-links] -- [Interaktivní komponenty |components] -- [AJAX & snippety |ajax] -- [Multiplier |multiplier] -- [Konfigurace |configuration] +Nette Application je jádrem frameworku Nette, které přináší výkonné nástroje pro vytváření moderních webových aplikací. Nabízí řadu výjimečných vlastností, které výrazně usnadňují vývoj a zlepšují bezpečnost i udržovatelnost kódu. Instalace @@ -26,10 +14,71 @@ Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:co composer require nette/application ``` + +Proč zvolit Nette Application? +------------------------------ + +Nette bylo vždy průkopníkem v oblasti webových technologií. + +**Obousměrný router:** Nette disponuje pokročilým routovacím systémem, který je unikátní svou obousměrností - nejen že překládá URL na akce aplikace, ale také dokáže zpětně generovat URL adresy. To znamená, že: +- Můžete kdykoliv změnit strukturu URL celé aplikace bez nutnosti upravovat šablony +- URL jsou automaticky kanonizovány, což zlepšuje SEO +- Routování je definováno na jednom místě, nikoliv roztroušeně v anotacích + +**Komponenty a signály:** Vestavěný komponentový systém inspirovaný Delphi a React.js je mezi PHP frameworky zcela výjimečný: +- Umožňuje vytvářet znovupoužitelné UI prvky +- Podporuje hierarchické skládání komponent +- Nabízí elegantní zpracování AJAX požadavků pomocí signálů +- Bohatá knihovna hotových komponent na [Componette](https://componette.org) + +**AJAX a snippety:** Nette představilo revoluční způsob práce s AJAXem již v roce 2009, dlouho před podobnými řešeními jako Hotwire pro Ruby on Rails nebo Symfony UX Turbo: +- Snippety umožňují aktualizovat jen části stránky bez nutnosti psát JavaScript +- Automatická integrace s komponentovým systémem +- Chytrá invalidace částí stránek +- Minimální množství přenášených dat + +**Intuitivní šablony [Latte|latte:]:** Nejbezpečnější šablonovací systém pro PHP s pokročilými funkcemi: +- Automatická ochrana proti XSS s kontextově citlivým escapováním +- Rozšiřitelnost pomocí vlastních filtrů, funkcí a značek +- Dědičnost šablon a snippety pro AJAX +- Vynikající podpora PHP 8.x s typovým systémem + +**Dependency Injection:** Nette plně využívá Dependency Injection: +- Automatické předávání závislostí (autowiring) +- Konfigurace pomocí přehledného NEON formátu +- Podpora pro továrny na komponenty + + +Hlavní výhody +------------- + +- **Bezpečnost**: Automatická obrana proti [zranitelnostem|nette:vulnerability-protection] jako XSS, CSRF, atd. +- **Produktivita**: Méně psaní, více funkcí díky chytrému návrhu +- **Debugging**: [Tracy debugger|tracy:] s routovacím panelem +- **Výkon**: Chytrá cache, lazy loading komponent +- **Flexibilita**: Snadná úprava URL i po dokončení aplikace +- **Komponenty**: Unikátní systém znovupoužitelných UI prvků +- **Moderní**: Plná podpora PHP 8.4+ a typového systému + + +Začínáme +-------- + +1. [Jak fungují aplikace? |how-it-works] - Pochopení základní architektury +2. [Presentery |presenters] - Práce s presentery a akcemi +3. [Šablony |templates] - Tvorba šablon v Latte +4. [Routování |routing] - Konfigurace URL adres +5. [Interaktivní komponenty |components] - Využití komponentového systému + + +Kompatbility s PHP +------------------ + | verze | kompatibilní s PHP |-----------|------------------- -| Nette Application 4.0 | PHP 8.0 – 8.2 -| Nette Application 3.1 | PHP 7.2 – 8.2 +| Nette Application 4.0 | PHP 8.1 – 8.4 +| Nette Application 3.2 | PHP 8.1 – 8.4 +| Nette Application 3.1 | PHP 7.2 – 8.3 | Nette Application 3.0 | PHP 7.1 – 8.0 | Nette Application 2.4 | PHP 5.6 – 8.0 diff --git a/application/cs/@left-menu.texy b/application/cs/@left-menu.texy index 9c512de998..68d0549dc9 100644 --- a/application/cs/@left-menu.texy +++ b/application/cs/@left-menu.texy @@ -1,10 +1,10 @@ -Aplikace v Nette -**************** +Nette Application +***************** - [Jak fungují aplikace? |how-it-works] -- [Bootstrap] +- [Bootstrapping] - [Presentery |presenters] - [Šablony |templates] -- [Moduly |modules] +- [Adresářová struktura |directory-structure] - [Routování |routing] - [Vytváření odkazů URL |creating-links] - [Interaktivní komponenty |components] @@ -15,5 +15,8 @@ Aplikace v Nette Další četba *********** +- [Proč používat Nette? |www:10-reasons-why-nette] +- [Instalace |nette:installation] +- [Píšeme první aplikaci! |quickstart:] - [Návody a postupy |best-practices:] - [Řešení problémů |nette:troubleshooting] diff --git a/application/cs/@meta.texy b/application/cs/@meta.texy new file mode 100644 index 0000000000..462d9add80 --- /dev/null +++ b/application/cs/@meta.texy @@ -0,0 +1 @@ +{{sitename: Nette Dokumentace}} diff --git a/application/cs/ajax.texy b/application/cs/ajax.texy index 6c0358218c..327977ea19 100644 --- a/application/cs/ajax.texy +++ b/application/cs/ajax.texy @@ -3,36 +3,43 @@ AJAX & snippety
-Moderní webové aplikace dnes běží napůl na serveru, napůl v prohlížeči. AJAX je tím klíčovým spojovacím prvkem. Jakou podporu nabízí Nette Framework? -- posílání výřezů šablony (tzv. snippety) +V éře moderních webových aplikací, kde se často rozkládá funkcionalita mezi serverem a prohlížečem, je AJAX nezbytným spojovacím prvkem. Jaké možnosti nám v této oblasti nabízí Nette Framework? +- odesílání částí šablony, tzv. snippetů - předávání proměnných mezi PHP a JavaScriptem -- debugování AJAXových aplikací +- nástroje pro debugování AJAXových požadavků
-AJAXový požadavek lze detekovat metodou služby [zapouzdřující HTTP požadavek |http:request] `$httpRequest->isAjax()` (detekuje podle HTTP hlavičky `X-Requested-With`). Uvnitř presenteru je k dispozici "zkratka" v podobě metody `$this->isAjax()`. -AJAXový požadavek se nijak neliší od klasického požadavku - je zavolán presenter s určitým view a parametry. Je také věcí presenteru, jak bude na něj reagovat: může použít vlastní rutinu, která vrátí nějaký fragment HTML kódu (HTML snippet), XML dokument, JSON objekt nebo kód v JavaScriptu. +AJAXový požadavek +================= -Pro odesílání dat prohlížeči ve formátu JSON lze využít předpřipravený objekt `payload`: +AJAXový požadavek se v zásadě neliší od klasického HTTP požadavku. Zavolá se presenter s určitými parametry. A je na presenteru, jakým způsobem bude na požadavek reagovat - může vrátit data ve formátu JSON, odeslat část HTML kódu, XML dokument, atd. -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +Na straně prohlížeče inicializujeme AJAXový požadavek pomocí funkce `fetch()`: + +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // zpracování odpovědi +}); ``` -Pokud potřebujete plnou kontrolu nad odeslaným JSONem, použijte metodu `sendJson` v presenteru. Tím ihned ukončíte činnost presenteru a obejdete se i bez šablony: +Na straně serveru rozpoznáme AJAXový požadavek metodou `$httpRequest->isAjax()` služby [zapouzdřující HTTP požadavek |http:request]. K detekci používá HTTP hlavičku `X-Requested-With`, proto je důležité ji odesílat. V rámci presenteru lze použít metodu `$this->isAjax()`. + +Chcete-li odeslat data ve formátu JSON, použijte metodu [`sendJson()` |presenters#Odeslání odpovědi]. Metoda rovněž ukončí činnost presenteru. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Když chceme odeslat HTML, můžeme jednak zvolit speciální šablonu pro AJAX: +Máte-li v plánu odpovědět pomocí speciální šablony určené pro AJAX, můžete to udělat následovně: ```php public function handleClick($param): void @@ -45,10 +52,20 @@ public function handleClick($param): void ``` +Snippety +======== + +Nejsilnější prostředek, který nabízí Nette pro propojení serveru s klientem, představují snippety. Díky nim můžete z obyčejné aplikace udělat AJAXovou jen s minimálním úsilím a několika řádky kódu. Jak to celé funguje demonstruje příklad Fifteen, jehož kód najdete na [GitHubu |https://github.com/nette-examples/fifteen]. + +Snippety, nebo-li výstřižky, umožnují aktualizovat jen části stránky, místo toho, aby se celá stránka znovunačítala. Jednak je to rychlejší a efektivnější, ale poskytuje to také komfortnější uživatelský zážitek. Snippety vám mohou připomínat Hotwire pro Ruby on Rails nebo Symfony UX Turbo. Zajímavé je, že Nette představilo snippety již o 14 let dříve. + +Jak snippety fungují? Při prvním načtení stránky (ne-AJAXovém požadavku) se načte celá stránka včetně všech snippetů. Když uživatel interaguje se stránkou (např. klikne na tlačítko, odešle formulář, atd.), místo načtení celé stránky se vyvolá AJAXový požadavek. Kód v presenteru provede akci a rozhodne, které snippety je třeba aktualizovat. Nette tyto snippety vykreslí a odešle ve formě pole ve formátu JSON. Obslužný kód v prohlížeči získané snippety vloží zpět do stránky. Přenáší se tedy jen kód změněných snippetů, což šetří šířku pásma a zrychluje načítání oproti přenášení obsahu celé stránky. + + Naja -==== +---- -K obsluze AJAXových požadavků na straně prohlížeče slouží [knihovna Naja |https://naja.js.org]. Tu [nainstalujte |https://naja.js.org/#/guide/01-install-setup-naja] jako node.js balíček (pro použití s aplikacemi Webpack, Rollup, Vite, Parcel a dalšími): +K obsluze snippetů na straně prohlížeče slouží [knihovna Naja |https://naja.js.org]. Tu [nainstalujte |https://naja.js.org/#/guide/01-install-setup-naja] jako node.js balíček (pro použití s aplikacemi Webpack, Rollup, Vite, Parcel a dalšími): ```shell npm install naja @@ -60,59 +77,55 @@ npm install naja ``` +Nejprve je potřeba knihovnu [inicializovat |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: -Snippety -======== +```js +naja.initialize(); +``` -Daleko silnější nástroj představuje vestavěná podpora AJAXových snippetů. Díky ní lze udělat z obyčejné aplikace AJAXovou prakticky několika řádky kódu. Jak to celé funguje, demonstruje příklad Fifteen, jehož kód najdete na [GitHubu |https://github.com/nette-examples/fifteen]. +Aby se z obyčejného odkazu (signálu) nebo odeslání formuláře vytvořil AJAXový požadavek, stačí označit příslušný odkaz, formulář nebo tlačítko třídou `ajax`: -Snippety fungují tak, že při prvotním (tedy neAJAXovém) požadavku se přenese celá stránka a poté se při každém již AJAXovém [subrequestu |components#Signál] (= požadavku na stejný presenter a view) přenáší pouze kód změněných částí ve zmíněném úložišti `payload`. K tomu slouží dva mechanismy: invalidace a renderování snippetů. +```html +Go -Snippety vám mohou připomínat Hotwire pro Ruby on Rails nebo Symfony UX Turbo, nicméně Nette s nimi přišlo už o čtrnáct let dříve. +
+ +
+nebo + +
+ +
+``` -Invalidace snippetů -=================== -Každý objekt třídy [Control |components] (což je i samotný Presenter) si umí zapamatovat, jestli při signálu došlo ke změnám, které si vyžadují jej překreslit. K tomu slouží dvojice metod `redrawControl()` a `isControlInvalid()`. Příklad: +Překreslení snippetů +-------------------- + +Každý objekt třídy [Control |components] (včetně samotného Presenteru) eviduje, zda došlo ke změnám vyžadujícím jeho překreslení. K tomu slouží metoda `redrawControl()`: ```php public function handleLogin(string $user): void { - // po přihlášení uživatele se musí objekt překreslit + // po přihlášení je potřeba překreslit relevantní část $this->redrawControl(); // ... } ``` -Nette však nabízí ještě jemnější rozlišení, než na úrovni komponent. Uvedené metody mohou totiž jako argument přijímat název tzv. "snippetu", nebo-li výstřižku. Lze tedy invalidovat (rozuměj: vynutit překreslení) na úrovni těchto snippetů (každý objekt může mít libovolné množství snippetů). Pokud se invaliduje celá komponenta, tak se i každý snippet překreslí. Komponenta je "invalidní" i tehdy, pokud je invalidní některá její subkomponenta. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // invaliduje snippet 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, alespoň jeden snippet je invalid +Nette umožňuje ještě jemnější kontrolu toho, co se má překreslit. Uvedená metoda totiž může jako argument přijímat název snippetu. Lze tedy invalidovat (rozuměj: vynutit překreslení) na úrovni částí šablony. Pokud se invaliduje celá komponenta, tak se překreslí i každý její snippet: -$this->redrawControl(); // invaliduje celou komponentu, každý snippet -$this->isControlInvalid('footer'); // -> true +```php +// invaliduje snippet 'header' +$this->redrawControl('header'); ``` -Komponenta, která přijímá signál, je automaticky označena za invalidní. - -Díky invalidaci snippetů přesně víme, které části kterých prvků bude potřeba překreslit. - - -Tagy `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================= - -Vykreslování stránky probíhá velmi podobně jako při běžném požadavku: načtou se stejné šablony atd. Podstatné však je vynechání částí, které se nemají dostat na výstup; ostatní části se přiřadí k identifikátoru a pošlou se uživateli ve formátu srozumitelném pro obslužný program JavaScriptu. - -Syntaxe -------- +Snippety v Latte +---------------- -Pokud se uvnitř šablony nachází control nebo snippet, musíme jej obalit párovou značkou `{snippet} ... {/snippet}` - ty totiž zajistí, že se vykreslený snippet vystřihne a pošle do prohlížeče. Také jej obalí pomocnou značkou `
` s vygenerovaným `id`. V uvedeném příkladě je snippet pojmenován jako `header` a může představovat i například šablonu controlu: +Používání snippetů v Latte je nesmírně snadné. Chcete-li definovat část šablony jako snippet, obalte ji jednoduše značkami `{snippet}` a `{/snippet}`: ```latte {snippet header} @@ -120,7 +133,9 @@ Pokud se uvnitř šablony nachází control nebo snippet, musíme jej obalit pá {/snippet} ``` -Snippetu jiného typu než `
` nebo snippetu s dalšími HTML atributy docílíme použitím atributové varianty: +Snippet vytvoří v HTML stránce element `
` se speciálním vygenerovaným `id`. Při překreslení snippetu se pak aktulizuje obsah tohoto elementu. Proto je nutné, aby při prvotním vykreslení stránky se vykreslily také všechny snippety, byť mohou být třeba na začátku prázdné. + +Můžete vytvořit i snippet s jiným elementem než `
` pomocí n:attributu: ```latte
@@ -129,138 +144,106 @@ Snippetu jiného typu než `
` nebo snippetu s dalšími HTML atributy docí ``` -Dynamické snippety -================== +Oblasti snippetů +---------------- -Nette také umožňuje používání snippetů, jejichž název se vytvoří až za běhu - tj. dynamicky. Hodí se to pro různé seznamy, kde při změně jednoho řádku nechceme přenášet AJAXem celý seznam, ale stačí onen samotný řádek. Příklad: +Názvy snippetů mohou být také výrazy: ```latte -
    - {foreach $list as $id => $item} -
  • {$item} update
  • - {/foreach} -
+{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Zde máme statický snippet `itemsContainer`, obsahující několik dynamických snippetů `item-0`, `item-1` atd. +Takto nám vznikne několik snippetů `item-0`, `item-1` atd. Pokud bychom přímo invalidovali dynamický snippet (například `item-1`), nepřekreslilo by se nic. Důvod je ten, že snippety opravdu fungují jako výstřižky a vykreslují se jen přímo ony samotné. Jenže v šabloně fakticky žádný snippet pojmenovaný `item-1` není. Ten vznikne až vykonáváním kódu v okolí snippetu, tedy cyklu foreach. Označíme proto část šablony, která se má vykonat pomocí značky `{snippetArea}`: -Dynamické snippety nelze invalidovat přímo (invalidace `item-1` neudělá vůbec nic), musíte invalidovat jim nadřazený statický snippet (zde snippet `itemsContainer`). Potom dojde k tomu, že se provede celý kód toho kontejneru, ale prohlížeči se pošlou jenom jeho sub-snippety. Pokud chcete, aby prohlížeč dostal pouze jediný z nich, musíte upravit vstup toho kontejneru tak, aby ostatní negeneroval. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -V příkladu výše zkrátka musíte zajistit, aby při ajaxovém požadavku byla v proměnné `$list` pouze jedna položka a tedy aby ten cyklus `foreach` naplnil pouze jeden dynamický snippet: +A necháme překreslit jak samotný snippet, tak i celou nadřazenou oblast: ```php -class HomepagePresenter extends Nette\Application\UI\Presenter -{ - /** - * Tato metoda vrací data pro seznam. - * Obvykle se jedná pouze o vyžádání dat z modelu. - * Pro účely tohoto příkladu jsou data zadána natvrdo. - */ - private function getTheWholeList(): array - { - return [ - 'První', - 'Druhý', - 'Třetí' - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Zároveň je vhodné zajistit, aby pole `$items` obsahovalo jen ty položky, které se mají překreslit. -Snippety v includované šabloně -============================== - -Může se stát, že máme snippet v šabloně, kterou teprve includujeme do jiné šablony. V takovém případě je nutné vkládání této šablony obalit značkami `snippetArea`, které pak invalidujeme spolu se samotnym snippetem. - -Tagy `snippetArea` zaručí, že se daný kód, který vkládá šablonu, provede, do prohlížeče se však odešle pouze snippet v includované šabloně. +Pokud do šablony vkládáme pomocí značky `{include}` jinou šablonu, která obsahuje snippety, je nutné vložení šablony opět zahrnout do `snippetArea` a tu invalidovat společně se snippetem: ```latte -{* parent.latte *} -{snippetArea wrapper} -{include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Tento přístup se nechá použít i v kombinaci s dynamickými snippety. - -Přidávání a mazání -================== +Snippety v komponentách +----------------------- -Pokud přidáte novou položku a invalidujete `itemsContainer`, pak vám AJAXový požadavek sice vrátí i nový snippet, ale obslužný javascript ho neumí nikam přiřadit. Na stránce totiž zatím není žádný HTML prvek s takovým ID. - -V takovém případě je nejjednodušší celý ten seznam obalit ještě jedním snippetem a invalidovat to celé: +Snippety můžete vytvářet i v [komponentách|components] a Nette je bude automaticky překreslovat. Ale platí tu určité omezení: pro překreslení snippetů volá metodu `render()` bez parametrů. Tedy nebude fungovat předávání parametrů v šabloně: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +nebude fungovat: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Posílání uživatelských dat +-------------------------- + +Společně se snippety můžete klientovi poslat libovolná další data. Stačí je zapsat do objektu `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + // ... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Totéž platí i pro mazání. Sice by se dal nějak poslat prázdný snippet, jenže v praxi jsou většinou seznamy stránkované a řešit úsporněji smazání jednoho plus případné načtení jiného (který se předtím nevešel) by bylo příliš složité. - -Posílání parametrů do komponenty -================================ +Předávání parametrů +=================== Pokud komponentě pomocí AJAXového požadavku odesíláme parametry, ať už parametry signálu nebo persistentní parametry, musíme u požadavku uvést jejich globální název, který obsahuje i jméno komponenty. Celý název parametru vrací metoda `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -A handle metoda s odpovídajícími parametry v komponentě. +A handle metoda s odpovídajícími parametry v komponentě: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/cs/bootstrap.texy b/application/cs/bootstrap.texy deleted file mode 100644 index fdb5decb4d..0000000000 --- a/application/cs/bootstrap.texy +++ /dev/null @@ -1,233 +0,0 @@ -Bootstrap -********* - -
    - -Bootstrap je zaváděcí kód, který inicializuje prostředí, vytvoří dependency injection (DI) kontejner a spustí aplikaci. Řekneme si: - -- jak se konfiguruje pomocí NEON souborů -- jak rozlišit produkční a vývojářský režim -- jak vytvořit DI kontejner - -
    - - -Aplikace, ať už jde o ty webové nebo skripty spouštěné z příkazové řádky, začínají svůj běh nějakou formou inicializace prostředí. V dávných dobách to míval na starosti soubor s názvem třeba `include.inc.php`, který prvotní soubor inkludoval. -V moderních Nette aplikacích jej nahradila třída `Bootstrap`, kterou jakožto součást aplikace najdete v souboru `app/Bootstrap.php`. Může vypadat kupříkladu takto: - -```php -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - public static function boot(): Configurator - { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; - } -} -``` - - -index.php -========= - -Prvotní soubor je v případě webových aplikací `index.php`, který se nachází ve veřejném adresáři `www/`. Ten si nechá od třídy Bootstrap inicializovat prostředí a vrátit `$configurator` a následně vyrobí DI kontejner. Poté z něj získá službu `Application`, kterou spustí webovou aplikaci: - -```php -// inicializace prostředí + získání objektu Configurator -$configurator = App\Bootstrap::boot(); -// vytvoření DI kontejneru -$container = $configurator->createContainer(); -// DI kontejner vytvoří objekt Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// spuštění Nette aplikace -$application->run(); -``` - -Jak vidno, s nastavením prostředí a vytvořením dependency injection (DI) kontejneru pomáhá třída [api:Nette\Bootstrap\Configurator], kterou si nyní blíže představíme. - - -Vývojářský vs produkční režim -============================= - -Nette rozlišuje dva základní režimy, ve kterých se požadavek vykoná: vývojářský a produkční. Vývojářský je zaměřen na maximální pohodlí programátora, zobrazuje se Tracy, automaticky se aktualizuje cache při změně šablon nebo konfigurace DI kontejneru, atd. Produkční je zaměřený na výkon a ostré nasazení, Tracy chyby pouze loguje a změny šablon a dalších souborů se netestují. - -Volba režimu se provádí autodetekcí, takže obvykle není potřeba nic konfigurovat nebo ručně přepínat. Režim je vývojářský tehdy, pokud je aplikace spuštěna na localhostu (tj. IP adresa `127.0.0.1` nebo `::1`) a není přitomna proxy (tj. její HTTP hlavička). Jinak běží v produkčním režimu. - -Pokud chceme vývojářský režim povolit i v dalších případech, například programátorům přistupujícím z konkrétní IP adresy, použijeme `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); // lze uvést i pole IP adres -``` - -Rozhodně doporučujeme kombinovat IP adresu s cookie. Do cookie `nette-debug` uložíme tajný token, např. `secret1234`, a tímto způsobem aktivujeme vývojářský režim pro programátory přistupující z konkrétní IP adresy a zároveň mající v cookie zmíněný token: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Vývojářský režim můžeme také vypnout úplně, i pro localhost: - -```php -$configurator->setDebugMode(false); -``` - -Pozor, hodnota `true` zapne vývojářský režim natvrdo, což se nikdy nesmí stát na produkčním serveru. - - -Debugovací nástroj Tracy -======================== - -Pro snadné debugování ještě zapneme skvělý nástroj [Tracy |tracy:]. Ve vývojářském režimu chyby vizualizuje a v produkčním režimu chyby loguje do uvedeného adresáře: - -```php -$configurator->enableTracy($appDir . '/log'); -``` - - -Dočasné soubory -=============== - -Nette využívá cache pro DI kontejner, RobotLoader, šablony atd. Proto je nutné nastavit cestu k adresáři, kam se bude cache ukládat: - -```php -$configurator->setTempDirectory($appDir . '/temp'); -``` - -Na Linuxu nebo macOS nastavte adresářům `log/` a `temp/` [práva pro zápis |nette:troubleshooting#Nastavení práv adresářů]. - - -RobotLoader -=========== - -Zpravidla budeme chtít automaticky načítat třídy pomocí [RobotLoaderu |robot-loader:], musíme ho tedy nastartovat a necháme jej načítat třídy z adresáře, kde je umístěný `Bootstrap.php` (tj. `__DIR__`), a všech podadresářů: - -```php -$configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Alternativní přístup je nechat třídy načítat pouze přes [Composer |best-practices:composer] při dodržení PSR-4. - - -Timezone -======== - -Přes konfigurátor můžete nastavit výchozí časovou zónu. - -```php -$configurator->setTimeZone('Europe/Prague'); -``` - - -Konfigurace DI kontejneru -========================= - -Součástí bootovacího procesu je vytvoření DI kontejneru neboli továrny na objekty, což je srdce celé aplikace. Jde vlastně o PHP třídu, kterou vygeneruje Nette a uloží do adresáře s cache. Továrna vyrábí klíčové objekty aplikace a pomocí konfiguračních souborů jí instruujeme, jak je má vytvářet a nastavovat, čímž ovlivňujeme chování celé aplikace. - -Konfigurační soubory se obvykle zapisují ve formátu [NEON |neon:format]. V samostatné kapitole se dočtete, [co vše lze konfigurovat |nette:configuring]. - -.[tip] -Ve vývojářském režimu se kontejner automaticky aktualizuje při každé změně kódu nebo konfiguračních souborů. V produkčním režimu se vygeneruje jen jednou a změny se kvůli maximalizaci výkonu nekontrolují. - -Konfigurační soubory načteme pomocí `addConfig()`: - -```php -$configurator->addConfig($appDir . '/config/common.neon'); -``` - -Pokud chceme přidat více konfiguračních souborů, můžeme funkci `addConfig()` zavolat vícekrát. - -```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); -if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); -} -``` - -Název `cli.php` není překlep, konfigurace může být zapsaná také v PHP souboru, který ji vrátí jako pole. - -Také můžeme přidat další konfigurační soubory v [sekci `includes` |dependency-injection:configuration#Vkládání souborů]. - -Pokud se v konfiguračních souborech objeví prvky se stejnými klíči, budou přepsány, nebo v případě [polí sloučeny |dependency-injection:configuration#Slučování]. Později vkládaný soubor má vyšší prioritu než předchozí. Soubor, ve kterém je sekce `includes` uvedena, má vyšší prioritu než v něm inkludované soubory. - - -Statické parametry ------------------- - -Parametry používané v konfiguračních souborech můžeme definovat [v sekci `parameters`|dependency-injection:configuration#parametry] a také je předávat (či přepisovat) metodou `addStaticParameters()` (má alias `addParameters()`). Důležité je, že různé hodnoty parametrů způsobí vygenerování dalších DI kontejnerů, tedy dalších tříd. - -```php -$configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -Na parametr `projectId` se lze v konfiguraci odkázat obvyklým zápisem `%projectId%`. Třída Configurator automaticky přidává parametry `appDir`, `wwwDir`, `tempDir`, `vendorDir`, `debugMode` a `consoleMode`. - - -Dynamické parametry -------------------- - -Do kontejneru můžeme přidat i dynamické parametry, jejichž různé hodnoty na rozdíl od statických parameterů nezpůsobí generování nových DI kontejnerů. - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Jednoduše tak můžeme přidat např. environmentální proměnné, na které se pak lze v konfiguraci odkázat zápisem `%env.variable%`. - -```php -$configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Importované služby ------------------- - -Nyní už jdeme hlouběji. Ačkoliv je smyslem DI kontejneru objekty vyrábet, výjimečně může vzniknout potřeba do kontejneru existující objekt vložit. Uděláme to tak, že službu definujeme s příznakem `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -A v bootstrapu do kontejneru vložíme objekt: - -```php -$configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Odlišné prostředí -================= - -Nebojte se upravit třídu Bootstrap podle svých potřeb. Metodě `boot()` můžete přidat parametry pro rozlišení webových projektů nebo doplnit další metody, například `bootForTests()`, která inicializuje prostředí pro jednotkové testy, `bootForCli()` pro skripty volané z příkazové řádky atd. - -```php -public static function bootForTests(): Configurator -{ - $configurator = self::boot(); - Tester\Environment::setup(); // inicializace Nette Testeru - return $configurator; -} -``` diff --git a/application/cs/bootstrapping.texy b/application/cs/bootstrapping.texy new file mode 100644 index 0000000000..2b4a2dec58 --- /dev/null +++ b/application/cs/bootstrapping.texy @@ -0,0 +1,298 @@ +Bootstrapping +************* + +
    + +Bootstrapping je proces inicializace prostředí aplikace, vytvoření kontejneru pro dependency injection (DI) a spuštění aplikace. Budeme probírat: + +- jak třída Bootstrap inicializuje prostředí +- jak jsou aplikace konfigurovány pomocí NEON souborů +- jak rozlišovat mezi produkčním a vývojářským režimem +- jak vytvořit a nakonfigurovat DI kontejner + +
    + + +Aplikace, ať už jde o ty webové nebo skripty spouštěné z příkazové řádky, začínají svůj běh nějakou formou inicializace prostředí. V dávných dobách to míval na starosti soubor s názvem třeba `include.inc.php`, který prvotní soubor inkludoval. V moderních Nette aplikacích jej nahradila třída `Bootstrap`, kterou jakožto součást aplikace najdete v souboru `app/Bootstrap.php`. Může vypadat kupříkladu takto: + +```php +use Nette\Bootstrap\Configurator; + +class Bootstrap +{ + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // Konfigurátor je zodpovědný za nastavení prostředí aplikace a služeb. + $this->configurator = new Configurator; + // Nastaví adresář pro dočasné soubory generované Nette (např. zkompilované šablony) + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void + { + // Nette je chytré a vývojový režim se zapíná automaticky, + // nebo jej můžete povolit pro konkrétní IP adresu odkomentováním následujícího řádku: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Aktivuje Tracy: ultimátní "švýcarský nůž" pro ladění. + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: automaticky načítá všechny třídy ve zvoleném adresáři + $this->configurator->createRobotLoader() + ->addDirectory(__DIR__) + ->register(); + } + + private function setupContainer(): void + { + // Načte konfigurační soubory + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); + } +} +``` + + +index.php +========= + +Prvotní soubor je v případě webových aplikací `index.php`, který se nachází ve [veřejném adresáři |directory-structure#Veřejný adresář www] `www/`. Ten si nechá od třídy Bootstrap inicializovat prostředí a vyrobit DI kontejner. Poté z něj získá službu `Application`, která spustí webovou aplikaci: + +```php +$bootstrap = new App\Bootstrap; +// Inicializace prostředí + vytvoření DI kontejneru +$container = $bootstrap->bootWebApplication(); +// DI kontejner vytvoří objekt Nette\Application\Application +$application = $container->getByType(Nette\Application\Application::class); +// Spuštění aplikace Nette a zpracování příchozího požadavku +$application->run(); +``` + +Jak vidno, s nastavením prostředí a vytvořením dependency injection (DI) kontejneru pomáhá třída [api:Nette\Bootstrap\Configurator], kterou si nyní blíže představíme. + + +Vývojářský vs produkční režim +============================= + +Nette se chová různě podle toho, zda běží na vývojářském nebo produkčním serveru: + +🛠️ Vývojářský režim (Development): + - Zobrazuje Tracy debugbar s užitečnými informacemi (SQL dotazy, čas vykonání, použitá paměť) + - Při chybě zobrazí detailní chybovou stránku s voláním funkcí a obsahem proměnných + - Automaticky obnovuje cache při změně Latte šablon, úpravě konfiguračních souborů atd. + + +🚀 Produkční režim (Production): + - Nezobrazuje žádné ladící informace, všechny chyby zapisuje do logu + - Při chybě zobrazí ErrorPresenter nebo obecnou stránku "Server Error" + - Cache se nikdy automaticky neobnovuje! + - Optimalizovaný pro rychlost a bezpečnost + + +Volba režimu se provádí autodetekcí, takže obvykle není potřeba nic konfigurovat nebo ručně přepínat: + +- vývojářský režim: na localhostu (IP adresa `127.0.0.1` nebo `::1`) pokud není přítomná proxy (tj. její HTTP hlavička) +- produkční režim: všude jinde + +Pokud chceme vývojářský režim povolit i v dalších případech, například programátorům přistupujícím z konkrétní IP adresy, použijeme `setDebugMode()`: + +```php +$this->configurator->setDebugMode('23.75.345.200'); // lze uvést i pole IP adres +``` + +Rozhodně doporučujeme kombinovat IP adresu s cookie. Do cookie `nette-debug` uložíme tajný token, např. `secret1234`, a tímto způsobem aktivujeme vývojářský režim pro programátory přistupující z konkrétní IP adresy a zároveň mající v cookie zmíněný token: + +```php +$this->configurator->setDebugMode('secret1234@23.75.345.200'); +``` + +Vývojářský režim můžeme také vypnout úplně, i pro localhost: + +```php +$this->configurator->setDebugMode(false); +``` + +Pozor, hodnota `true` zapne vývojářský režim natvrdo, což se nikdy nesmí stát na produkčním serveru. + + +Debugovací nástroj Tracy +======================== + +Pro snadné debugování ještě zapneme skvělý nástroj [Tracy |tracy:]. Ve vývojářském režimu chyby vizualizuje a v produkčním režimu chyby loguje do uvedeného adresáře: + +```php +$this->configurator->enableTracy($this->rootDir . '/log'); +``` + + +Dočasné soubory +=============== + +Nette využívá cache pro DI kontejner, RobotLoader, šablony atd. Proto je nutné nastavit cestu k adresáři, kam se bude cache ukládat: + +```php +$this->configurator->setTempDirectory($this->rootDir . '/temp'); +``` + +Na Linuxu nebo macOS nastavte adresářům `log/` a `temp/` [práva pro zápis |nette:troubleshooting#Nastavení práv adresářů]. + + +RobotLoader +=========== + +Zpravidla budeme chtít automaticky načítat třídy pomocí [RobotLoaderu |robot-loader:], musíme ho tedy nastartovat a necháme jej načítat třídy z adresáře, kde je umístěný `Bootstrap.php` (tj. `__DIR__`), a všech podadresářů: + +```php +$this->configurator->createRobotLoader() + ->addDirectory(__DIR__) + ->register(); +``` + +Alternativní přístup je nechat třídy načítat pouze přes [Composer |best-practices:composer] při dodržení PSR-4. + + +Timezone +======== + +Přes konfigurátor můžete nastavit výchozí časovou zónu. + +```php +$this->configurator->setTimeZone('Europe/Prague'); +``` + + +Konfigurace DI kontejneru +========================= + +Součástí bootovacího procesu je vytvoření DI kontejneru neboli továrny na objekty, což je srdce celé aplikace. Jde vlastně o PHP třídu, kterou vygeneruje Nette a uloží do adresáře s cache. Továrna vyrábí klíčové objekty aplikace a pomocí konfiguračních souborů jí instruujeme, jak je má vytvářet a nastavovat, čímž ovlivňujeme chování celé aplikace. + +Konfigurační soubory se obvykle zapisují ve formátu [NEON |neon:format]. V samostatné kapitole se dočtete, [co vše lze konfigurovat |nette:configuring]. + +.[tip] +Ve vývojářském režimu se kontejner automaticky aktualizuje při každé změně kódu nebo konfiguračních souborů. V produkčním režimu se vygeneruje jen jednou a změny se kvůli maximalizaci výkonu nekontrolují. + +Konfigurační soubory načteme pomocí `addConfig()`: + +```php +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); +``` + +Pokud chceme přidat více konfiguračních souborů, můžeme funkci `addConfig()` zavolat vícekrát. + +```php +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); +if (PHP_SAPI === 'cli') { + $this->configurator->addConfig($configDir . '/cli.php'); +} +``` + +Název `cli.php` není překlep, konfigurace může být zapsaná také v PHP souboru, který ji vrátí jako pole. + +Také můžeme přidat další konfigurační soubory v [sekci `includes` |dependency-injection:configuration#Vkládání souborů]. + +Pokud se v konfiguračních souborech objeví prvky se stejnými klíči, budou přepsány, nebo v případě [polí sloučeny |dependency-injection:configuration#Slučování]. Později vkládaný soubor má vyšší prioritu než předchozí. Soubor, ve kterém je sekce `includes` uvedena, má vyšší prioritu než v něm inkludované soubory. + + +Statické parametry +------------------ + +Parametry používané v konfiguračních souborech můžeme definovat [v sekci `parameters` |dependency-injection:configuration#Parametry] a také je předávat (či přepisovat) metodou `addStaticParameters()` (má alias `addParameters()`). Důležité je, že různé hodnoty parametrů způsobí vygenerování dalších DI kontejnerů, tedy dalších tříd. + +```php +$this->configurator->addStaticParameters([ + 'projectId' => 23, +]); +``` + +Na parametr `projectId` se lze v konfiguraci odkázat obvyklým zápisem `%projectId%`. + + +Dynamické parametry +------------------- + +Do kontejneru můžeme přidat i dynamické parametry, jejichž různé hodnoty na rozdíl od statických parameterů nezpůsobí generování nových DI kontejnerů. + +```php +$this->configurator->addDynamicParameters([ + 'remoteIp' => $_SERVER['REMOTE_ADDR'], +]); +``` + +Jednoduše tak můžeme přidat např. environmentální proměnné, na které se pak lze v konfiguraci odkázat zápisem `%env.variable%`. + +```php +$this->configurator->addDynamicParameters([ + 'env' => getenv(), +]); +``` + + +Výchozí parametry +----------------- + +V konfiguračních souborech můžete využít tyto statické parametry: + +- `%appDir%` je absolutní cesta k adresáři se souborem `Bootstrap.php` +- `%wwwDir%` je absolutní cesta k adresáři se vstupním souborem `index.php` +- `%tempDir%` je absolutní cesta k adresáři pro dočasné soubory +- `%vendorDir%` je absolutní cesta k adresáři, kam Composer instaluje knihovny +- `%rootDir%` je absolutní cesta ke kořenovému adresáři projektu +- `%baseUrl%` je absolutní URL ke kořenovému adresáři +- `%debugMode%` udává, zda je aplikace v debugovacím režimu +- `%consoleMode%` udává, zda request přišel přes příkazovou řádku + + +Importované služby +------------------ + +Nyní už jdeme hlouběji. Ačkoliv je smyslem DI kontejneru objekty vyrábet, výjimečně může vzniknout potřeba do kontejneru existující objekt vložit. Uděláme to tak, že službu definujeme s příznakem `imported: true`. + +```neon +services: + myservice: + type: App\Model\MyCustomService + imported: true +``` + +A v bootstrapu do kontejneru vložíme objekt: + +```php +$this->configurator->addServices([ + 'myservice' => new App\Model\MyCustomService('foobar'), +]); +``` + + +Odlišné prostředí +================= + +Nebojte se upravit třídu Bootstrap podle svých potřeb. Metodě `bootWebApplication()` můžete přidat parametry pro rozlišení webových projektů. Nebo můžeme doplnit další metody, například `bootTestEnvironment()`, která inicializuje prostředí pro jednotkové testy, `bootConsoleApplication()` pro skripty volané z příkazové řádky atd. + +```php +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // inicializace Nette Testeru + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container +{ + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); +} +``` diff --git a/application/cs/components.texy b/application/cs/components.texy index ff58589bbd..43a7f0f72b 100644 --- a/application/cs/components.texy +++ b/application/cs/components.texy @@ -87,7 +87,7 @@ class PollControl extends Control Vykreslení ========== -Už víme, že k vykreslení komponenty se používá značka `{control componentName}`. Ta vlastně zavolá metodu `render()` komponenty, ve které se postáráme o vykreslení. K dispozici máme, úplně stejně jako v presenteru, [Latte|latte:] šablonu v proměnné `$this->template`, do které předáme parametry. Na rozdíl od presenteru musíme uvést soubor se šablonou a nechat ji vykreslit: +Už víme, že k vykreslení komponenty se používá značka `{control componentName}`. Ta vlastně zavolá metodu `render()` komponenty, ve které se postáráme o vykreslení. K dispozici máme, úplně stejně jako v presenteru, [Latte šablonu|templates] v proměnné `$this->template`, do které předáme parametry. Na rozdíl od presenteru musíme uvést soubor se šablonou a nechat ji vykreslit: ```php .{file:PollControl.php} public function render(): void @@ -175,7 +175,7 @@ Komponenty, stejně jako presentery, předávají do šablon několik užitečn - `$user` je objekt [reprezentující uživatele |security:authentication] - `$presenter` je aktuální presenter - `$control` je aktuální komponenta -- `$flashes` pole [zpráv |#flash zprávy] zaslaných funkcí `flashMessage()` +- `$flashes` pole [zpráv |#Flash zprávy] zaslaných funkcí `flashMessage()` Signál @@ -192,13 +192,13 @@ public function handleClick(int $x, int $y): void } ``` -Odkaz, který zavolá signál, vytvoříme obvyklým způsobem, tedy v šabloně atributem `n:href` nebo značkou `{link}`, v kódu metodou `link()`. Více v kapitole [Vytváření odkazů URL|creating-links#Odkazy na signál]. +Odkaz, který zavolá signál, vytvoříme obvyklým způsobem, tedy v šabloně atributem `n:href` nebo značkou `{link}`, v kódu metodou `link()`. Více v kapitole [Vytváření odkazů URL |creating-links#Odkazy na signál]. ```latte click here ``` -Signál se vždy volá na aktuálním presenteru a view, tudíž není možné jej vyvolat na jiném presenteru nebo view. +Signál se vždy volá na aktuálním presenteru a action, není možné jej vyvolat na jiném presenteru nebo jiné action. Signál tedy způsobí znovunačtení stránky úplně stejně jako při původním požadavku, jen navíc zavolá obslužnou metodu signálu s příslušnými parametry. Pokud metoda neexistuje, vyhodí se výjimka [api:Nette\Application\UI\BadSignalException], která se uživateli zobrazí jako chybová stránka 403 Forbidden. @@ -230,36 +230,61 @@ $this->redirect(/* ... */); // a přesměrujeme ``` -Persistentní parametry -====================== +Přesměrování po signálu +======================= -Často se stává, že je v komponentách potřeba držet nějaký parametr pro uživatele po celou dobu, kdy se s komponentou pracuje. Může to být například číslo stránky ve stránkování. Takový parametr označíme jako persistentní pomocí anotace `@persistent`. +Po zpracování signálu komponenty často následuje přesměrování. Je to podobná situace jako u formulářů - po jejich odeslání také přesměrováváme, aby při obnovení stránky v prohlížeči nedošlo k opětovnému odeslání dat. ```php -class PollControl extends Control -{ - /** @persistent */ - public $page = 1; -} +$this->redirect('this') // přesměruje na aktuální presenter a action ``` -Tento parametr bude automaticky přenášen v každém odkazu jako GET parametr, a to až do chvíle, kdy uživatel stránku s touto komponentou opustí. +Protože komponenta je znovupoužitelný prvek a obvykle by neměla mít přímou vazbu na konkrétní presentery, metody `redirect()` a `link()` automaticky interpretují parametr jako signál komponenty: -.[caution] -Nikdy slepě nevěřte persistentním parametrům, protože mohou být snadno podvrženy (přepsáním v URL adrese stránky). Ověřte si například, zda je číslo stránky v platném rozsahu. +```php +$this->redirect('click') // přesměruje na signál 'click' téže komponenty +``` -V PHP 8 můžete pro označení persistentních parametrů použít také atributy: +Pokud potřebujete přesměrovat na jiný presenter či akci, můžete to udělat prostřednictvím presenteru: ```php -use Nette\Application\Attributes\Persistent; +$this->getPresenter()->redirect('Product:show'); // přesměruje na jiný presenter/action +``` -class PollControl extends Control + +Persistentní parametry +====================== + +Persistentní parametry slouží k udržování stavu v komponentách mezi různými požadavky. Jejich hodnota zůstává stejná i po kliknutí na odkaz. Na rozdíl od dat v session se přenášejí v URL. A to zcela automaticky, včetně odkazů vytvořených v jiných komponentách na téže stránce. + +Máte např. komponentu pro stránkování obsahu. Takových komponent může být na stránce několik. A přejeme si, aby po kliknutí na odkaz zůstaly všechny komponenty na své aktuální stránce. Proto z čísla stránky (`page`) uděláme persistentní parametr. + +Vytvoření persistentního parametru je v Nette nesmírně jednoduché. Stačí vytvořit veřejnou property a označit ji atributem: (dříve se používalo `/** @persistent */`) + +```php +use Nette\Application\Attributes\Persistent; // tento řádek je důležitý + +class PaginatingControl extends Control { #[Persistent] - public $page = 1; + public int $page = 1; // musí být public } ``` +U property doporučujeme uvádět i datový typ (např. `int`) a můžete uvést i výchozí hodnotu. Hodnoty parametrů lze [validovat |#Validace persistentních parametrů]. + +Při vytváření odkazu lze persistentnímu parametru změnit hodnotu: + +```latte +next +``` + +Nebo jej lze *vyresetovat*, tj. odstranit z URL. Pak bude nabývat svou výchozí hodnotu: + +```latte +reset +``` + Persistentní komponenty ======================= @@ -299,22 +324,16 @@ Vezměme si jako příklad komponentu, která má závislost na službě `PollFa ```php class PollControl extends Control { - /** @var PollFacade */ - private $facade; - - /** @var int Id ankety pro kterou vytváříme komponentu */ - private $id; - - public function __construct(int $id, PollFacade $facade) - { - $this->facade = $facade; - $this->id = $id; + public function __construct( + private int $id, // Id ankety pro kterou vytváříme komponentu + private PollFacade $facade, + ) { } public function handleVote(int $voteId): void { - $this->facade->vote($id, $voteId); - //... + $this->facade->vote($this->id, $voteId); + // ... } } ``` @@ -328,12 +347,9 @@ Správným řešením je napsat pro komponentu továrnu, tedy třídu, která n ```php class PollControlFactory { - /** @var PollFacade */ - private $facade; - - public function __construct(PollFacade $facade) - { - $this->facade = $facade; + public function __construct( + private PollFacade $facade, + ) { } public function create(int $id): PollControl @@ -353,14 +369,11 @@ services: a nakonec ji použijeme v našem presenteru: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { - /** @var PollControlFactory */ - private $pollControlFactory; - - public function __construct(PollControlFactory $pollControlFactory) - { - $this->pollControlFactory = $pollControlFactory; + public function __construct( + private PollControlFactory $pollControlFactory, + ) { } protected function createComponentPollControl(): PollControl @@ -389,12 +402,12 @@ Komponenty do hloubky Komponenty v Nette Application představují znovupoužitelné součásti webové aplikace, které vkládáme do stránek a kterým se ostatně věnuje celá tato kapitola. Jaké přesně schopnosti taková komponenta má? 1) je vykreslitelná v šabloně -2) ví, kterou svou část má vykreslit při [AJAXovém požadavku |ajax#invalidace] (snippety) -3) má schopnost ukládat svůj stav do URL (persistetní parametry) +2) ví, [kterou svou část |ajax#Snippety] má vykreslit při AJAXovém požadavku (snippety) +3) má schopnost ukládat svůj stav do URL (persistentní parametry) 4) má schopnost reagovat na uživatelské akce (signály) 5) vytváří hierarchickou strukturu (kde kořenem je presenter) -Každou z těchto funkcí obstarává některá z tříd dědičné linie. Vykreslování (1 + 2) má na starosti [api:Nette\Application\UI\Control], začlenění do [životního cyklu |presenters#zivotni-cyklus-presenteru] (3, 4) třída [api:Nette\Application\UI\Component] a vytváření hierachické struktury (5) třídy [Container a Component |component-model:]. +Každou z těchto funkcí obstarává některá z tříd dědičné linie. Vykreslování (1 + 2) má na starosti [api:Nette\Application\UI\Control], začlenění do [životního cyklu |presenters#Životní cyklus presenteru] (3, 4) třída [api:Nette\Application\UI\Component] a vytváření hierachické struktury (5) třídy [Container a Component |component-model:]. ``` Nette\ComponentModel\Component { IComponent } @@ -415,6 +428,33 @@ Nette\ComponentModel\Component { IComponent } [* lifecycle-component.svg *] *** *Životní cyklus componenty* .<> +Validace persistentních parametrů +--------------------------------- + +Hodnoty [persistentních parametrů |#Persistentní parametry] přijatých z URL zapisuje do properties metoda `loadState()`. Ta také kontroluje, zda odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí. + +Nikdy slepě nevěřte persistentním parametrům, protože mohou být snadno uživatelem přepsány v URL. Takto například ověříme, zda je číslo stránky `$this->page` větší než 0. Vhodnou cestou je přepsat zmíněnou metodu `loadState()`: + +```php +class PaginatingControl extends Control +{ + #[Persistent] + public int $page = 1; + + public function loadState(array $params): void + { + parent::loadState($params); // zde se nastaví $this->page + // následuje vlastní kontrola hodnoty: + if ($this->page < 1) { + $this->error(); + } + } +} +``` + +Opačný proces, tedy sesbírání hodnot z persistentních properties, má na starosti metoda `saveState()`. + + Signály do hloubky ------------------ @@ -426,7 +466,7 @@ Signál může přijímat jakákoliv komponenta, presenter nebo objekt, který i Mezi hlavní příjemce signálů budou patřit `Presentery` a vizuální komponenty dědící od `Control`. Signál má sloužit jako znamení pro objekt, že má něco udělat – anketa si má započítat hlas od uživatele, blok s novinkami se má rozbalit a zobrazit dvakrát tolik novinek, formulář byl odeslán a má zpracovat data a podobně. -URL pro signál vytváříme pomocí metody [Component::link() |api:Nette\Application\UI\Component::link()]. Jako parametr `$destination` předáme řetězec `{signal}!` a jako `$args` pole argumentů, které chceme signálu předat. Signál se vždy volá na aktuální view s aktuálními parametry, parametry signálu se jen přidají. Navíc se přidává hned na začátku **parametr `?do`, který určuje signál**. +URL pro signál vytváříme pomocí metody [Component::link() |api:Nette\Application\UI\Component::link()]. Jako parametr `$destination` předáme řetězec `{signal}!` a jako `$args` pole argumentů, které chceme signálu předat. Signál se vždy volá na aktuálním presenteru a action s aktuálními parametry, parametry signálu se jen přidají. Navíc se přidává hned na začátku **parametr `?do`, který určuje signál**. Jeho formát je buď `{signal}`, nebo `{signalReceiver}-{signal}`. `{signalReceiver}` je název komponenty v presenteru. Proto nemůže být v názvu komponenty pomlčka – používá se k oddělení názvu komponenty a signálu, je ovšem možné takto zanořit několik komponent. @@ -443,27 +483,3 @@ if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, ' ``` Tím je signál provedený předčasně a už se nebude znovu volat. - - -/--comment - /** @var callable[]&(callable(Component $sender): void)[]; Occurs when component is attached to presenter */ - public $onAnchor; - - /** - * Loads state informations. - */ - public function loadState(array $params): void - - /** - * Saves state informations for next request. - */ - public function saveState(array &$params): void - - - /** - * Returns destination as Link object. - * @param string $destination in format "[homepage] [[[module:]presenter:]action | signal! | this] [#fragment]" - * @param array|mixed $args - */ - public function lazyLink(string $destination, $args = []): Link -\-- diff --git a/application/cs/configuration.texy b/application/cs/configuration.texy index cfa573b0e2..b105f5154e 100644 --- a/application/cs/configuration.texy +++ b/application/cs/configuration.texy @@ -14,10 +14,14 @@ application: debugger: ... # (bool) výchozí je true # bude se při chybě volat error-presenter? - catchExceptions: ... # (bool) výchozí je true v produkčním režimu + # má efekt pouze ve vývojářském režimu + catchExceptions: ... # (bool) výchozí je true # název error-presenteru - errorPresenter: Error # (string) výchozí je 'Nette:Error' + errorPresenter: Error # (string|array) výchozí je 'Nette:Error' + + # definuje aliasy pro presentery a akce + aliases: ... # definuje pravidla pro překlad názvu presenteru na třídu mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) výchozí je false ``` -Protože ve vývojovém režimu se error-presentery standardně nevolají a chybu zobrazí až Tracy, změnou hodnoty `catchExceptions` na `true` můžeme při vývoji ověřit jejich správnou funkčnost. +Od `nette/application` verze 3.2 lze definovat dvojici error-presenterů: + +```neon +application: + errorPresenter: + 4xx: Error4xx # pro výjimku Nette\Application\BadRequestException + 5xx: Error5xx # pro ostatní výjimky +``` + +Volba `silentLinks` určuje, jak se Nette zachová ve vývojářském režimu, když selže generování odkazu (třeba proto, že neexistuje presenter, atd). Výchozí hodnota `false` znamená, že Nette vyhodí `E_USER_WARNING` chybu. Nastavením na `true` dojde k potlačení této chybové hlášky. V produkčním prostředí se `E_USER_WARNING` vyvolá vždy. Toto chování můžeme také ovlivnit nastavením proměnné presenteru [$invalidLinkMode |creating-links#Neplatné odkazy]. -Volba `silentLinks` určuje, jak se Nette zachová ve vývojářském režimu, když selže generování odkazu (třeba proto, že neexistuje presenter, atd). Výchozí hodnota `false` znamená, že Nette vyhodí `E_USER_WARNING` chybu. Nastavením na `true` dojde k potlačení této chybové hlášky. V produkčním prostředí se `E_USER_WARNING` vyvolá vždy. Toto chování můžeme také ovlivnit nastavením proměnné presenteru [$invalidLinkMode|creating-links#neplatne-odkazy]. +[Aliasy zjednodušují odkazování |creating-links#Aliasy] na často používané presentery. -[Mapování definuje pravidla |modules#mapování], podle kterých se z názvu presenteru odvodí název třídy. +[Mapování definuje pravidla |directory-structure#Mapování presenterů], podle kterých se z názvu presenteru odvodí název třídy. Automatická registrace presenterů @@ -73,22 +86,28 @@ latte: # zobrazit Latte panel v Tracy Baru pro hlavní šablonu (true) nebo všechny komponenty (all)? debugger: ... # (true|false|'all') výchozí je true - # přepne Latte do XHTML režimu (deprecated) - xhtml: ... # (bool) výchozí je false - # generuje šablony s hlavičkou declare(strict_types=1) strictTypes: ... # (bool) výchozí je false + # zapne režim [striktního parseru |latte:develop#striktní režim] + strictParsing: ... # (bool) výchozí je false + + # aktivuje [kontrolu vygenerovaného kódu |latte:develop#Kontrola vygenerovaného kódu] + phpLinter: ... # (string) výchozí je null + + # nastaví locale + locale: cs_CZ # (string) výchozí je null + # třída objektu $this->template templateClass: App\MyTemplateClass # výchozí je Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -Pokud používáte Latte verze 3, můžete přidávat nové [rozšíření |latte:creating-extension] pomocí: +Pokud používáte Latte verze 3, můžete přidávat nové [rozšíření |latte:extending-latte#Latte Extension] pomocí: ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Pokud používáte Latte verze 2, můžete registrovat nové tagy (makra) buď uvedením jména třídy, nebo referencí na službu. Jako výchozí je zavolána metoda `install()`, ale to lze změnit tím, že uvedeme jméno jiné metody: @@ -137,10 +156,10 @@ Vytváření PHP konstant. ```neon constants: - FOOBAR: 'baz' + Foobar: 'baz' ``` -Po nastartování aplikace bude vytvořena konstanta `FOOBAR`. +Po nastartování aplikace bude vytvořena konstanta `Foobar`. .[note] Konstanty by neměly sloužit jako jakési globálně dostupné proměnné. Pro předávání hodnot do objektů využijte [dependency injection |dependency-injection:passing-dependencies]. @@ -155,3 +174,18 @@ Nastavení direktiv PHP. Přehled všech direktiv naleznete na [php.net |https:/ php: date.timezone: Europe/Prague ``` + + +Služby DI +========= + +Tyto služby se přidávají do DI kontejneru: + +| Název | Typ | Popis +|---------------------------------------------------------- +| `application.application` | [api:Nette\Application\Application] | [spouštěč celé aplikace |how-it-works#Nette Application] +| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] +| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | továrna na presentery +| `application.###` | [api:Nette\Application\UI\Presenter] | jednotlivé presentery +| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | továrna objektu `Latte\Engine` +| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | továrna pro [`$this->template` |templates] diff --git a/application/cs/creating-links.texy b/application/cs/creating-links.texy index bfe6f58a57..0263cbbc4a 100644 --- a/application/cs/creating-links.texy +++ b/application/cs/creating-links.texy @@ -24,7 +24,7 @@ Nejčastěji vytváříme odkazy v šablonách a skvělým pomocníkem je atribu detail ``` -Všimněte si, že místo HTML atributu `href` jsme použili [n:atribut |latte:syntax#n-atributy] `n:href`. Jeho hodnotou pak není URL, jak by tomu bylo v případě atributu `href`, ale název presenteru a akce. +Všimněte si, že místo HTML atributu `href` jsme použili [n:atribut |latte:syntax#n:atributy] `n:href`. Jeho hodnotou pak není URL, jak by tomu bylo v případě atributu `href`, ale název presenteru a akce. Kliknutí na odkaz je, zjednodušeně řečeno, něco jako zavolání metody `ProductPresenter::renderShow()`. A pokud má ve své signatuře parametry, můžeme ji volat s argumenty: @@ -38,7 +38,7 @@ Je možné předávat i pojmenované parametry. Následující odkaz předává detail produktu ``` -Pokud metoda `ProductPresenter::renderShow()` nemá `$lang` ve své signatuře, může si hodnotu parametru zjistit pomocí `$lang = $this->getParameter('lang')`. +Pokud metoda `ProductPresenter::renderShow()` nemá `$lang` ve své signatuře, může si hodnotu parametru zjistit pomocí `$lang = $this->getParameter('lang')` nebo z [property |presenters#Parametry požadavku]. Pokud jsou parametry uložené v poli, lze je rozvinout operátorem `...` (v Latte 2.x operátorem `(expand)`): @@ -47,12 +47,12 @@ Pokud jsou parametry uložené v poli, lze je rozvinout operátorem `...` (v Lat detail produktu ``` -V odkazech se také automaticky předávají tzv. [persistentní parametry|presenters#persistentní parametry]. +V odkazech se také automaticky předávají tzv. [persistentní parametry |presenters#Persistentní parametry]. Atribut `n:href` je velmi šikovný pro HTML značky ``. Chceme-li odkaz vypsat jinde, například v textu, použijeme `{link}`: ```latte -Adresa je: {link Homepage:default} +Adresa je: {link Home:default} ``` @@ -88,7 +88,7 @@ Formát podporují všechny značky Latte a všechny metody presenteru, které s Základním tvarem je tedy `Presenter:action`: ```latte -úvodní stránka +úvodní stránka ``` Pokud odkazujeme na akci aktuálního presenteru, můžeme jeho název vynechat: @@ -100,17 +100,17 @@ Pokud odkazujeme na akci aktuálního presenteru, můžeme jeho název vynechat: Pokud je cílem akce `default`, můžeme ji vynechat, ale dvojtečka musí zůstat: ```latte -úvodní stránka +úvodní stránka ``` -Odkazy mohou také směřovat do jiných [modulů |modules]. Zde se odkazy rozlišují na relativní do zanořeného submodulu, nebo absolutní. Princip je analogický k cestám na disku, jen místo lomítek jsou dvojtečky. Předpokládejme, že aktuální presenter je součástí modulu `Front`, potom zapíšeme: +Odkazy mohou také směřovat do jiných [modulů |directory-structure#Presentery a šablony]. Zde se odkazy rozlišují na relativní do zanořeného submodulu, nebo absolutní. Princip je analogický k cestám na disku, jen místo lomítek jsou dvojtečky. Předpokládejme, že aktuální presenter je součástí modulu `Front`, potom zapíšeme: ```latte odkaz na Front:Shop:Product:show odkaz na Admin:Product:show ``` -Speciálním případem je odkaz [na sebe sama|#Odkaz na aktuální stránku], kdy jako cíl uvedeme `this`. +Speciálním případem je odkaz [na sebe sama |#Odkaz na aktuální stránku], kdy jako cíl uvedeme `this`. ```latte refresh @@ -119,7 +119,7 @@ Speciálním případem je odkaz [na sebe sama|#Odkaz na aktuální stránku], k Odkazovat můžeme na určitou část stránky přes tzv. fragment za znakem mřížky `#`: ```latte -odkaz na Homepage:default a fragment #main +odkaz na Home:default a fragment #main ``` @@ -128,7 +128,7 @@ Absolutní cesty Odkazy generované pomocí `link()` nebo `n:href` jsou vždy absolutní cesty (tj. začínají znakem `/`), ale nikoliv absolutní URL s protokolem a doménou jako `https://domain`. -Pro vygenerování absolutní URL přidejte na začátek dvě lomítka (např. `n:href="//Homepage:"`). Nebo lze přepnout presenter, aby generoval jen absolutní odkazy nastavením `$this->absoluteUrls = true`. +Pro vygenerování absolutní URL přidejte na začátek dvě lomítka (např. `n:href="//Home:"`). Nebo lze přepnout presenter, aby generoval jen absolutní odkazy nastavením `$this->absoluteUrls = true`. Odkaz na aktuální stránku @@ -140,7 +140,7 @@ Cíl `this` vytvoří odkaz na aktuální stránku: refresh ``` -Zároveň se přenáší i všechny parametry uvedené v signatuře `render()` nebo `action()` metody. Takže pokud jsme na stránce `Product:show` a `id: 123`, odkaz na `this` předá i tento parameter. +Zároveň se přenáší i všechny parametry uvedené v signatuře metody `action()` nebo `render()`, pokud není `action()` definovaná. Takže pokud jsme na stránce `Product:show` a `id: 123`, odkaz na `this` předá i tento parameter. Samozřejmě je možné parametry specifikovat přímo: @@ -165,7 +165,7 @@ Parametry jsou stejné jako u metody `link()`, navíc je však možné místo ko V kombinaci s `n:href` v jednom elementu se dá použít zkrácená podoba: ```latte -... +... ``` Zástupný znak `*` lze použít pouze místo akce, nikoliv presenteru. @@ -182,7 +182,7 @@ Pro zjištění, zda jsme v určitém modulu nebo jeho submodulu, použijeme met Odkazy na signál ================ -Cílem odkazu nemusí být jen presenter a akce, ale také [signál|components#Signál] (volají metodu `handle()`). Pak je syntaxe následující: +Cílem odkazu nemusí být jen presenter a akce, ale také [signál |components#Signál] (volají metodu `handle()`). Pak je syntaxe následující: ``` [//] [sub-component:]signal! [#fragment] @@ -213,27 +213,51 @@ Protože [komponenty|components] jsou samostatné znovupoužitelné celky, kter Pokud bychom chtěli v šabloně komponenty odkazovat na presentery, použijeme k tomu značku `{plink}`: ```latte -úvod +úvod ``` nebo v kódu ```php -$this->getPresenter()->link('Homepage:default') +$this->getPresenter()->link('Home:default') ``` +Aliasy .{data-version:v3.2.2} +============================= + +Občas se může hodit přiřadit dvojici Presenter:akce snadno zapamatovatelný alias. Například úvodní stránku `Front:Home:default` pojmenovat jednoduše jako `home` nebo `Admin:Dashboard:default` jako `admin`. + +Aliasy se definují v [konfiguraci|configuration] pod klíčem `application › aliases`: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +V odkazech se pak zapisují pomocí zavináče, například: + +```latte +administrace +``` + +Podporované jsou i ve všech metodách pracujících s odkazy, jako je `redirect()` a podobně. + + Neplatné odkazy =============== Může se stát, že vytvoříme neplatný odkaz - buď proto, že vede na neexistující presenter, nebo proto, že předává víc parametrů, než které cílová metoda přijímá ve své signatuře, nebo když pro cílovou akci nelze vygenerovat URL. Jak naložit s neplatnými odkazy určuje statická proměnná `Presenter::$invalidLinkMode`. Ta může nabývat kombinaci těchto hodnot (konstant): -- `Presenter::INVALID_LINK_SILENT` - tichý režim, jako URL se vrátí znak # -- `Presenter::INVALID_LINK_WARNING` - vyhodí se varování E_USER_WARNING, které bude v produkčním režimu zalogováno, ale nezpůsobí přerušení běhu skriptu -- `Presenter::INVALID_LINK_TEXTUAL` - vizuální varování, vypíše chybu přímo do odkazu -- `Presenter::INVALID_LINK_EXCEPTION` - vyhodí se výjimka InvalidLinkException +- `Presenter::InvalidLinkSilent` - tichý režim, jako URL se vrátí znak # +- `Presenter::InvalidLinkWarning` - vyhodí se varování E_USER_WARNING, které bude v produkčním režimu zalogováno, ale nezpůsobí přerušení běhu skriptu +- `Presenter::InvalidLinkTextual` - vizuální varování, vypíše chybu přímo do odkazu +- `Presenter::InvalidLinkException` - vyhodí se výjimka InvalidLinkException -Výchozí nastavení je `INVALID_LINK_WARNING` v produkčním režimu a `INVALID_LINK_WARNING | INVALID_LINK_TEXTUAL` ve vývojovém. `INVALID_LINK_WARNING` v produkčním prostředí nezpůsobí přerušení skriptu, ale varování bude zalogováno. Ve vývojovém prostředí ho zachytí [Tracy |tracy:] a zobrazí bluescreen. `INVALID_LINK_TEXTUAL` pracuje tak, že jako URL vrátí chybovou zprávu, která začíná znaky `#error:`. Aby takové odkazy byly na první pohled patrné, doplníme si do CSS: +Výchozí nastavení je `InvalidLinkWarning` v produkčním režimu a `InvalidLinkWarning | InvalidLinkTextual` ve vývojovém. `InvalidLinkWarning` v produkčním prostředí nezpůsobí přerušení skriptu, ale varování bude zalogováno. Ve vývojovém prostředí ho zachytí [Tracy |tracy:] a zobrazí bluescreen. `InvalidLinkTextual` pracuje tak, že jako URL vrátí chybovou zprávu, která začíná znaky `#error:`. Aby takové odkazy byly na první pohled patrné, doplníme si do CSS: ```css a[href^="#error:"] { @@ -257,6 +281,6 @@ Jak vytvářet odkazy s podobným komfortem jako má metoda `link()`, ale bez p LinkGenerátor je služba, kterou si můžete nechat předat přes konstruktor a poté vytvářet odkazy jeho metodou `link()`. -Oproti presenterům je tu rozdíl. LinkGenerator vytváří všechny odkazy rovnou jako absolutní URL. A dále neexistuje žádný "aktuální presenter", takže nelze jako cíl uvést jen název akce `link('default')` nebo uvádět relativní cesty k [modulům |modules]. +Oproti presenterům je tu rozdíl. LinkGenerator vytváří všechny odkazy rovnou jako absolutní URL. A dále neexistuje žádný "aktuální presenter", takže nelze jako cíl uvést jen název akce `link('default')` nebo uvádět relativní cesty k modulům. Neplatné odkazy vždy vyhazují `Nette\Application\UI\InvalidLinkException`. diff --git a/application/cs/directory-structure.texy b/application/cs/directory-structure.texy new file mode 100644 index 0000000000..c5d98980df --- /dev/null +++ b/application/cs/directory-structure.texy @@ -0,0 +1,526 @@ +Adresářová struktura aplikace +***************************** + +
    + +Jak navrhnout přehlednou a škálovatelnou adresářovou strukturu pro projekty v Nette Framework? Ukážeme si osvědčené postupy, které vám pomohou s organizací kódu. Dozvíte se: + +- jak **logicky rozčlenit** aplikaci do adresářů +- jak strukturu navrhnout tak, aby **dobře škálovala** s růstem projektu +- jaké jsou **možné alternativy** a jejich výhody či nevýhody + +
    + + +Důležité je zmínit, že Nette Framework samotný na žádné konkrétní struktuře nelpí. Je navržen tak, aby se dal snadno přizpůsobit jakýmkoliv potřebám a preferencím. + + +Základní struktura projektu +=========================== + +Přestože Nette Framework nediktuje žádnou pevnou adresářovou strukturu, existuje osvědčené výchozí uspořádání v podobě [Web Project|https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← adresář s aplikací +├── assets/ ← soubory SCSS, JS, obrázky..., alternativně resources/ +├── bin/ ← skripty pro příkazovou řádku +├── config/ ← konfigurace +├── log/ ← logované chyby +├── temp/ ← dočasné soubory, cache +├── tests/ ← testy +├── vendor/ ← knihovny instalované Composerem +└── www/ ← veřejný adresář (document-root) +\-- + +Tuto strukturu můžete libovolně upravovat podle svých potřeb - složky přejmenovat či přesouvat. Poté stačí pouze upravit relativní cesty k adresářům v souboru `Bootstrap.php` a případně `composer.json`. Nic víc není potřeba, žádná složitá rekonfigurace, žádné změny konstant. Nette disponuje chytrou autodetekcí a automaticky rozpozná umístění aplikace včetně její URL základny. + + +Principy organizace kódu +======================== + +Když poprví prozkoumáváte nový projekt, měli byste se v něm rychle zorientovat. Představte si, že rozkliknete adresář `app/Model/` a uvidíte tuto strukturu: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Z ní vyčtete jen to, že projekt používá nějaké služby, repozitáře a entity. O skutečném účelu aplikace se nedozvíte vůbec nic. + +Podívejme se na jiný přístup - **organizaci podle domén**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Tady je to jiné - na první pohled je jasné, že jde o e-shop. Už samotné názvy adresářů prozrazují, co aplikace umí - pracuje s platbami, objednávkami a produkty. + +První přístup (organizace podle typu tříd) přináší v praxi řadu problémů: kód, který spolu logicky souvisí, je roztříštěný do různých složek a musíte mezi nimi přeskakovat. Proto budeme organizovat podle domén. + + +Jmenné prostory +--------------- + +Je zvykem, že adresářová struktura koresponduje se jmennými prostory v aplikaci. To znamená, že fyzické umístění souborů odpovídá jejich namespace. Například třída umístěná v `app/Model/Product/ProductRepository.php` by měla mít namespace `App\Model\Product`. Tento princip pomáhá v orientaci v kódu a zjednodušuje autoloading. + + +Jednotné vs množné číslo v názvech +---------------------------------- + +Všimněte si, že u hlavních adresářů aplikace používáme jednotné číslo: `app`, `config`, `log`, `temp`, `www`. Stejně tak i uvnitř aplikace: `Model`, `Core`, `Presentation`. Je to proto, že každý z nich představuje jeden ucelený koncept. + +Podobně třeba `app/Model/Product` reprezentuje vše kolem produktů. Nenazveme to `Products`, protože nejde o složku plnou produktů (to by tam byly soubory `nokia.php`, `samsung.php`). Je to namespace obsahující třídy pro práci s produkty - `ProductRepository.php`, `ProductService.php`. + +Složka `app/Tasks` je v množném čísle proto, že obsahuje sadu samostatných spustitelných skriptů - `CleanupTask.php`, `ImportTask.php`. Každý z nich je samostatnou jednotkou. + +Pro konzistenci doporučujeme používat: +- Jednotné číslo pro namespace reprezentující funkční celek (byť pracující s více entitami) +- Množné číslo pro kolekce samostatných jednotek +- V případě nejistoty nebo pokud nad tím nechcete přemýšlet, zvolte jednotné číslo + + +Veřejný adresář `www/` +====================== + +Tento adresář je jediný přístupný z webu (tzv. document-root). Často se můžete setkat i s názvem `public/` místo `www/` - je to jen otázka konvence a na funkčnost rostlináře to nemá vliv. Adresář obsahuje: +- [Vstupní bod |bootstrapping#index.php] aplikace `index.php` +- Soubor `.htaccess` s pravidly pro mod_rewrite (u Apache) +- Statické soubory (CSS, JavaScript, obrázky) +- Uploadované soubory + +Pro správné zabezpečení aplikace je zásadní mít správně [nakonfigurovaný document-root |nette:troubleshooting#Jak změnit či ostranit z URL adresář www]. + +.[note] +Nikdy neumisťujte do tohoto adresáře složku `node_modules/` - obsahuje tisíce souborů, které mohou být spustitelné a neměly by být veřejně dostupné. + + +Aplikační adresář `app/` +======================== + +Toto je hlavní adresář s aplikačním kódem. Základní struktura: + +/--pre +app/ +├── Core/ ← infrastrukturní záležitosti +├── Model/ ← business logika +├── Presentation/ ← presentery a šablony +├── Tasks/ ← příkazové skripty +└── Bootstrap.php ← zaváděcí třída aplikace +\-- + +`Bootstrap.php` je [startovací třída aplikace|bootstrapping], která inicializuje prostředí, načítá konfiguraci a vytváří DI kontejner. + +Pojďme se nyní podívat na jednotlivé podadresáře podrobněji. + + +Presentery a šablony +==================== + +Prezentační část aplikace máme v adresáři `app/Presentation`. Alternativou je krátké `app/UI`. Je to místo pro všechny presentery, jejich šablony a případné pomocné třídy. + +Tuto vrstvu organizujeme podle domén. V komplexním projektu, který kombinuje e-shop, blog a API, by struktura vypadala takto: + +/--pre +app/Presentation/ +├── Shop/ ← e-shop frontend +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administrace +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← API endpointy + └── V1/ +\-- + +Naopak u jednoduchého blogu bychom použili členění: + +/--pre +app/Presentation/ +├── Front/ ← frontend webu +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administrace +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemapy atd. +\-- + +Složky jako `Home/` nebo `Dashboard/` obsahují presentery a šablony. Složky jako `Front/`, `Admin/` nebo `Api/` nazýváme **moduly**. Technicky jde o běžné adresáře, které slouží k logickému členění aplikace. + +Každá složka s presenterem obsahuje stejně pojmenovaný presenter a jeho šablony. Například složka `Dashboard/` obsahuje: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presenter +└── default.latte ← šablona +\-- + +Tato adresářová struktura se odráží ve jmenných prostorech tříd. Například `DashboardPresenter` se nachází ve jmenném prostoru `App\Presentation\Admin\Dashboard` (viz [#mapování presenterů]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + // ... +} +``` + +Na presenter `Dashboard` uvnitř modulu `Admin` odkazujeme v aplikaci pomocí dvojtečkové notace jako na `Admin:Dashboard`. Na jeho akci `default` potom jako na `Admin:Dashboard:default`. V případě zanořených modulů používáme více dvojteček, například `Shop:Order:Detail:default`. + + +Flexibilní vývoj struktury +-------------------------- + +Jednou z velkých výhod této struktury je, jak elegantně se přizpůsobuje rostoucím potřebám projektu. Jako příklad si vezměme část generující XML feedy. Na začátku máme jednoduchou podobu: + +/--pre +Export/ +├── ExportPresenter.php ← jeden presenter pro všechny exporty +├── sitemap.latte ← šablona pro sitemapu +└── feed.latte ← šablona pro RSS feed +\-- + +Časem přibydou další typy feedů a potřebujeme pro ně více logiky... Žádný problém! Složka `Export/` se jednoduše stane modulem: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── zbozi.latte ← feed pro Zboží.cz + └── heureka.latte ← feed pro Heureka.cz +\-- + +Tato transformace je naprosto plynulá - stačí vytvořit nové podsložky, rozdělit do nich kód a aktualizovat odkazy (např. z `Export:feed` na `Export:Feed:zbozi`). Díky tomu můžeme strukturu postupně rozšiřovat podle potřeby, úroveň zanoření není nijak omezena. + +Pokud například v administraci máte mnoho presenterů týkajících se správy objednávek, jako jsou `OrderDetail`, `OrderEdit`, `OrderDispatch` atd., můžete pro lepší organizovanost v tomto místě vytvořit modul (složku) `Order`, ve kterém budou (složky pro) presentery `Detail`, `Edit`, `Dispatch` a další. + + +Umístění šablon +--------------- + +V předchozích ukázkách jsme viděli, že šablony jsou umístěny přímo ve složce s presenterem: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presenter +├── DashboardTemplate.php ← volitelná třída pro šablonu +└── default.latte ← šablona +\-- + +Toto umístění se v praxi ukazuje jako nejpohodlnější - všechny související soubory máte hned po ruce. + +Alternativně můžete šablony umístit do podsložky `templates/`. Nette podporuje obě varianty. Dokonce můžete šablony umístit i úplně mimo `Presentation/` složku. Vše o možnostech umístění šablon najdete v kapitole [Hledání šablon |templates#Hledání šablon]. + + +Pomocné třídy a komponenty +-------------------------- + +K prezenterům a šablonám často patří i další pomocné soubory. Umístíme je logicky podle jejich působnosti: + +1. **Přímo u presenteru** v případě specifických komponent pro daný presenter: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← komponenta pro výpis produktů +└── FilterForm.php ← formulář pro filtrování +\-- + +2. **Pro modul** - doporučujeme využít složku `Accessory`, která se umístí přehledně hned na začátku abecedy: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← komponenty pro frontend +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **Pro celou aplikaci** - v `Presentation/Accessory/`: +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +Nebo můžete pomocné třídy jako `LatteExtension.php` nebo `TemplateFilters.php` umístit do infrastrukturní složky `app/Core/Latte/`. A komponenty do `app/Components`. Volba závisí na zvyklostech týmu. + + +Model - srdce aplikace +====================== + +Model obsahuje veškerou business logiku aplikace. Pro jeho organizaci platí opět pravidlo - strukturujeme podle domén: + +/--pre +app/Model/ +├── Payment/ ← vše kolem plateb +│ ├── PaymentFacade.php ← hlavní vstupní bod +│ ├── PaymentRepository.php +│ ├── Payment.php ← entita +├── Order/ ← vše kolem objednávek +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← vše kolem dopravy +\-- + +V modelu se typicky setkáte s těmito typy tříd: + +**Fasády**: představují hlavní vstupní bod do konkrétní domény v aplikaci. Působí jako orchestrátor, který koordinuje spolupráci mezi různými službami za účelem implementace kompletních use-cases (jako "vytvoř objednávku" nebo "zpracuj platbu"). Pod svojí orchestrační vrstvou fasáda skrývá implementační detaily před zbytkem aplikace, čímž poskytuje čisté rozhraní pro práci s danou doménou. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // validace + // vytvoření objednávky + // odeslání e-mailu + // zapsání do statistik + } +} +``` + +**Služby**: zaměřují se na specifickou business operaci v rámci domény. Na rozdíl od fasády, která orchestruje celé use-cases, služba implementuje konkrétní byznys logiku (jako výpočty cen nebo zpracování plateb). Služby jsou typicky bezstavové a mohou být použity buď fasádami jako stavební bloky pro komplexnější operace, nebo přímo jinými částmi aplikace pro jednodušší úkony. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // výpočet ceny + } +} +``` + +**Repozitáře**: zajišťují veškerou komunikaci s datovým úložištěm, typicky databází. Jeho úkolem je načítání a ukládání entit a implementace metod pro jejich vyhledávání. Repozitář odstiňuje zbytek aplikace od implementačních detailů databáze a poskytuje objektově orientované rozhraní pro práci s daty. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Entity**: objekty reprezentující hlavní byznys koncepty v aplikaci, které mají svou identitu a mění se v čase. Typicky jde o třídy mapované na databázové tabulky pomocí ORM (jako Nette Database Explorer nebo Doctrine). Entity mohou obsahovat business pravidla týkající se jejich dat a validační logiku. + +```php +// Entita mapovaná na databázovou tabulku orders +class Order extends Nette\Database\Table\ActiveRow +{ + public function addItem(Product $product, int $quantity): void + { + $this->related('order_items')->insert([ + 'product_id' => $product->id, + 'quantity' => $quantity, + 'unit_price' => $product->price, + ]); + } +} +``` + +**Value objekty**: neměnné objekty reprezentující hodnoty bez vlastní identity - například peněžní částka nebo e-mailová adresa. Dvě instance value objektu se stejnými hodnotami jsou považovány za identické. + + +Infrastrukturní kód +=================== + +Složka `Core/` (nebo také `Infrastructure/`) je domovem pro technický základ aplikace. Infrastrukturní kód typicky zahrnuje: + +/--pre +app/Core/ +├── Router/ ← routování a URL management +│ └── RouterFactory.php +├── Security/ ← autentizace a autorizace +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← logování a monitoring +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← cachovací vrstva +│ └── FullPageCache.php +└── Integration/ ← integrace s ext. službami + ├── Slack/ + └── Stripe/ +\-- + +U menších projektů pochopitelně stačí ploché členění: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +Jde o kód, který: + +- Řeší technickou infrastrukturu (routování, logování, cachování) +- Integruje externí služby (Sentry, Elasticsearch, Redis) +- Poskytuje základní služby pro celou aplikaci (mail, databáze) +- Je většinou nezávislý na konkrétní doméně - cache nebo logger funguje stejně pro eshop či blog. + +Tápete, jestli určitá třída patří sem, nebo do modelu? Klíčový rozdíl je v tom, že kód v `Core/`: + +- Neví nic o doméně (produkty, objednávky, články) +- Je většinou možné ho přenést do jiného projektu +- Řeší "jak to funguje" (jak poslat mail), nikoliv "co to dělá" (jaký mail poslat) + +Příklad pro lepší pochopení: + +- `App\Core\MailerFactory` - vytváří instance třídy pro odesílání e-mailů, řeší SMTP nastavení +- `App\Model\OrderMailer` - používá `MailerFactory` k odesílání e-mailů o objednávkách, zná jejich šablony a ví, kdy se mají poslat + + +Příkazové skripty +================= + +Aplikace často potřebují vykonávat činnosti mimo běžné HTTP požadavky - ať už jde o zpracování dat v pozadí, údržbu, nebo periodické úlohy. Pro spouštění slouží jednoduché skripty v adresáři `bin/`, samotnou implementační logiku pak umisťujeme do `app/Tasks/` (případně `app/Commands/`). + +Příklad: + +/--pre +app/Tasks/ +├── Maintenance/ ← údržbové skripty +│ ├── CleanupCommand.php ← mazání starých dat +│ └── DbOptimizeCommand.php ← optimalizace databáze +├── Integration/ ← integrace s externími systémy +│ ├── ImportProducts.php ← import z dodavatelského systému +│ └── SyncOrders.php ← synchronizace objednávek +└── Scheduled/ ← pravidelné úlohy + ├── NewsletterCommand.php ← rozesílání newsletterů + └── ReminderCommand.php ← notifikace zákazníkům +\-- + +Co patří do modelu a co do příkazových skriptů? Například logika pro odeslání jednoho e-mailu je součástí modelu, hromadná rozesílka tisíců e-mailů už patří do `Tasks/`. + +Úlohy obvykle [spouštíme z příkazového řádku |https://blog.nette.org/en/cli-scripts-in-nette-application] nebo přes cron. Lze je spouštět i přes HTTP požadavek, ale je nutné myslet na bezpečnost. Presenter, který úlohu spustí, je potřeba zabezpečit, například jen pro přihlášené uživatele nebo silným tokenem a přístupem z povolených IP adres. U dlouhých úloh je nutné zvýšit časový limit skriptu a použít `session_write_close()`, aby se nezamykala session. + + +Další možné adresáře +==================== + +Kromě zmíněných základních adresářů můžete podle potřeb projektu přidat další specializované složky. Podívejme se na nejčastější z nich a jejich použití: + +/--pre +app/ +├── Api/ ← logika pro API nezávislá na prezentační vrstvě +├── Database/ ← migrační skripty a seedery pro testovací data +├── Components/ ← sdílené vizuální komponenty napříč celou aplikací +├── Event/ ← užitečné pokud používáte event-driven architekturu +├── Mail/ ← e-mailové šablony a související logika +└── Utils/ ← pomocné třídy +\-- + +Pro sdílené vizuální komponenty používané v presenterech napříč aplikací lze použít složku `app/Components` nebo `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← sdílené formulářové komponenty +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← komponenty pro výpisy dat +│ └── DataGrid.php +└── Navigation/ ← navigační prvky + ├── Breadcrumbs.php + └── Menu.php +\-- + +Sem patří komponenty, které mají komplexnější logiku. Pokud chcete komponenty sdílet mezi více projekty, je vhodné je vyčlenit do samostatného composer balíčku. + +Do adresáře `app/Mail` můžete umístit správu e-mailové komunikace: + +/--pre +app/Mail/ +├── templates/ ← e-mailové šablony +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Mapování presenterů +=================== + +Mapování definuje pravidla pro odvozování názvu třídy z názvu presenteru. Specifikujeme je v [konfiguraci|configuration] pod klíčem `application › mapping`. + +Na této stránce jsme si ukázali, že presentery umísťujeme do složky `app/Presentation` (případně `app/UI`). Tuto konvenci musíme Nette sdělit v konfiguračním souboru. Stačí jeden řádek: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Jak mapování funguje? Pro lepší pochopení si nejprve představme aplikaci bez modulů. Chceme, aby třídy presenterů spadaly do jmenného prostoru `App\Presentation`, aby se presenter `Home` mapoval na třídu `App\Presentation\HomePresenter`. Což dosáhneme touto konfigurací: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +Mapování funguje tak, že název presenteru `Home` nahradí hvězdičku v masce `App\Presentation\*Presenter`, čímž získáme výsledný název třídy `App\Presentation\HomePresenter`. Jednoduché! + +Jak ale vidíte v ukázkách v této a dalších kapitolách, třídy presenterů umisťujeme do eponymních podadresářů, například presenter `Home` se mapuje na třídu `App\Presentation\Home\HomePresenter`. Toho dosáhneme zdvojením dvojtečky (vyžaduje Nette Application 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Nyní přistoupíme k mapování presenterů do modulů. Pro každý modul můžeme definovat specifické mapování: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +Podle této konfigurace se presenter `Front:Home` mapuje na třídu `App\Presentation\Front\Home\HomePresenter`, zatímco presenter `Api:OAuth` na třídu `App\Api\OAuthPresenter`. + +Protože moduly `Front` i `Admin` mají podobný způsob mapování a takových modulů bude nejspíš více, je možné vytvořit obecné pravidlo, které je nahradí. Do masky třídy tak přibude nová hvězdička pro modul: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Funguje to i pro hlouběji zanořené adresářové struktury, jako je například presenter `Admin:User:Edit`, se segment s hvězdičkou opakuje pro každou úroveň a výsledkem je třída `App\Presentation\Admin\User\Edit\EditPresenter`. + +Alternativním zápisem je místo řetězce použít pole skládající se ze tří segmentů. Tento zápis je ekvivaletní s předchozím: + +```neon +application: + mapping: + *: [App\Presentation, *, **Presenter] + Api: [App\Api, '', *Presenter] +``` diff --git a/application/cs/how-it-works.texy b/application/cs/how-it-works.texy index 625c515622..5e06fe853a 100644 --- a/application/cs/how-it-works.texy +++ b/application/cs/how-it-works.texy @@ -22,18 +22,18 @@ Adresářová struktura vypadá nějak takto: /--pre web-project/ ├── app/ ← adresář s aplikací -│ ├── Presenters/ ← presentery a šablony -│ │ ├── HomepagePresenter.php ← třída presenteru Homepage -│ │ └── templates/ ← adresář se šablonami -│ │ ├── @layout.latte ← šablona layoutu -│ │ └── Homepage/ ← šablony presenteru Homepage -│ │ └── default.latte ← šablona akce 'default' -│ ├── Router/ ← konfigurace URL adres +│ ├── Core/ ← základní třídy nutné pro chod +│ │ └── RouterFactory.php ← konfigurace URL adres +│ ├── Presentation/ ← presentery, šablony & spol. +│ │ ├── @layout.latte ← šablona layoutu +│ │ └── Home/ ← adresář presenteru Home +│ │ ├── HomePresenter.php ← třída presenteru Home +│ │ └── default.latte ← šablona akce default │ └── Bootstrap.php ← zaváděcí třída Bootstrap ├── bin/ ← skripty spouštěné z příkazové řádky ├── config/ ← konfigurační soubory │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← logované chyby ├── temp/ ← dočasné soubory, cache, … ├── vendor/ ← knihovny instalované Composerem @@ -45,9 +45,9 @@ Adresářová struktura vypadá nějak takto: └── .htaccess ← zakazuje přístup do všech adresářů krom www \-- -Adresářovou strukturu můžete jakkoliv měnit, složky přejmenovat či přesunout, a poté pouze upravit cesty k `log/` a `temp/` v souboru `Bootstrap.php` a dále cestu k tomuto souboru v `composer.json` v sekci `autoload`. Nic víc, žádná složitá rekonfigurace, žádné změny konstant. Nette totiž disponuje [chytrou autodetekcí|bootstrap#vyvojarsky-vs-produkcni-rezim]. +Adresářovou strukturu můžete jakkoliv měnit, složky přejmenovat či přesunout, je zcela flexibilní. Nette navíc disponuje chytrou autodetekcí a automaticky rozpozná umístění aplikace včetně její URL základny. -U trošku větších aplikací můžeme složky s presentery a šablonami rozčlenit na disku do podadresářů a třídy do jmenných prostorů, kterým říkáme [moduly |modules]. +U trošku větších aplikací můžeme složky s presentery a šablonami [rozčlenit do podadresářů |directory-structure#Presentery a šablony] a třídy do jmenných prostorů, kterým říkáme moduly. Adresář `www/` představuje tzv. veřejný adresář neboli document-root projektu. Můžete jej přejmenovat bez nutnosti cokoliv dalšího nastavovat na straně aplikace. Jen je potřeba [nakonfigurovat hosting |nette:troubleshooting#Jak změnit či ostranit z URL adresář www] tak, aby document-root mířil do tohoto adresáře. @@ -65,7 +65,7 @@ Aplikace WebProject je připravená ke spuštění, není třeba vůbec nic konf HTTP požadavek ============== -Vše začíná ve chvíli, kdy uživatel v prohlížeči otevře stránku. Tedy když prohlížeč zaklepe na server s HTTP požadavkem. Požadavek míří na jediný PHP soubor, který se nachází ve veřejném adresáři `www/`, a tím je `index.php`. Dejme tomu, že jde o požadavek na adresu `https://example.com/product/123`. Díky vhodnému [nastavení serveru|nette:troubleshooting#Jak nastavit server pro hezká URL?] se i tohle URL mapuje na soubor `index.php` a ten se vykoná. +Vše začíná ve chvíli, kdy uživatel v prohlížeči otevře stránku. Tedy když prohlížeč zaklepe na server s HTTP požadavkem. Požadavek míří na jediný PHP soubor, který se nachází ve veřejném adresáři `www/`, a tím je `index.php`. Dejme tomu, že jde o požadavek na adresu `https://example.com/product/123`. Díky vhodnému [nastavení serveru |nette:troubleshooting#Jak nastavit server pro hezká URL] se i tohle URL mapuje na soubor `index.php` a ten se vykoná. Jeho úkolem je: @@ -75,11 +75,11 @@ Jeho úkolem je: Jakou že továrnu? Nevyrábíme přece traktory, ale webové stránky! Vydržte, hned se to vysvětlí. -Slovy „inicializace prostředí“ myslíme například to, že se aktivuje [Tracy|tracy:], což je úžasný nástroj pro logování nebo vizualizaci chyb. Na produkčním serveru chyby loguje, na vývojovém rovnou zobrazuje. Tudíž k inicializaci patří i rozhodnutí, zda web běží v produkčním nebo vývojářském režimu. K tomu Nette používá autodetekci: pokud web spouštíte na localhost, běží v režimu vývojářském. Nemusíte tak nic konfigurovat a aplikace je rovnou připravena jak pro vývoj, tak ostré nasazení. Tyhle kroky se provádějí a jsou podrobně rozepsané v kapitole o [třídě Bootstrap|bootstrap]. +Slovy „inicializace prostředí“ myslíme například to, že se aktivuje [Tracy|tracy:], což je úžasný nástroj pro logování nebo vizualizaci chyb. Na produkčním serveru chyby loguje, na vývojovém rovnou zobrazuje. Tudíž k inicializaci patří i rozhodnutí, zda web běží v produkčním nebo vývojářském režimu. K tomu Nette používá [chytrou autodetekci |bootstrapping#Vývojářský vs produkční režim]: pokud web spouštíte na localhost, běží v režimu vývojářském. Nemusíte tak nic konfigurovat a aplikace je rovnou připravena jak pro vývoj, tak ostré nasazení. Tyhle kroky se provádějí a jsou podrobně rozepsané v kapitole o [třídě Bootstrap|bootstrapping]. Třetím bodem (ano, druhý jsme přeskočili, ale vrátíme se k němu) je spuštění aplikace. Vyřizování HTTP požadavků má v Nette na starosti třída `Nette\Application\Application` (dále `Application`), takže když říkáme spustit aplikaci, myslíme tím konkrétně zavolání metody s příznačným názvem `run()` na objektu této třídy. -Nette je mentor, který vás vede k psaní čistých aplikací podle osvědčených metodik. A jedna z těch naprosto nejosvědčenějších se nazývá **dependency injection**, zkráceně DI. V tuto chvíli vás nechceme zatěžovat vysvětlováním DI, od toho je tu [samostatná kapitola|dependency-injection:introduction], podstatný je důsledek, že klíčové objekty nám bude obvykle vytvářet továrna na objekty, které se říká **DI kontejner** (zkráceně DIC). Ano, to je ta továrna, o které byla před chvíli řeč. A vyrobí nám i objekt `Application`, proto potřebujeme nejprve kontejner. Získáme jej pomocí třídy `Configurator` a necháme jej vyrobit objekt `Application`, zavoláme na něm metodu `run()` a tím se spustí Nette aplikace. Přesně tohle se děje v souboru [index.php|bootstrap#index.php]. +Nette je mentor, který vás vede k psaní čistých aplikací podle osvědčených metodik. A jedna z těch naprosto nejosvědčenějších se nazývá **dependency injection**, zkráceně DI. V tuto chvíli vás nechceme zatěžovat vysvětlováním DI, od toho je tu [samostatná kapitola|dependency-injection:introduction], podstatný je důsledek, že klíčové objekty nám bude obvykle vytvářet továrna na objekty, které se říká **DI kontejner** (zkráceně DIC). Ano, to je ta továrna, o které byla před chvíli řeč. A vyrobí nám i objekt `Application`, proto potřebujeme nejprve kontejner. Získáme jej pomocí třídy `Configurator` a necháme jej vyrobit objekt `Application`, zavoláme na něm metodu `run()` a tím se spustí Nette aplikace. Přesně tohle se děje v souboru [index.php |bootstrapping#index.php]. Nette Application @@ -91,7 +91,7 @@ Aplikace psané v Nette se člení do spousty tzv. presenterů (v jiných framew Application začne tím, že požádá tzv. router, aby rozhodl, kterému z presenterů předat aktuální požadavek k vyřízení. Router rozhodne, čí je to zodpovědnost. Podívá se na vstupní URL `https://example.com/product/123` a na základě toho, jak je nastavený, rozhodne, že tohle je práce např. pro **presenter** `Product`, po kterém bude chtít jako **akci** zobrazení (`show`) produktu s `id: 123`. Dvojici presenter + akce je dobrým zvykem zapisovat oddělené dvojtečkou jako `Product:show`. -Tedy router transformoval URL na dvojici `Presenter:action` + parametry, v našem případě `Product:show` + `id: 123`. Jak takový router vypadá se můžete podívat v souboru `app/Router/RouterFactory.php` a podrobně ho popisujeme v kapitole [Routing]. +Tedy router transformoval URL na dvojici `Presenter:action` + parametry, v našem případě `Product:show` + `id: 123`. Jak takový router vypadá se můžete podívat v souboru `app/Core/RouterFactory.php` a podrobně ho popisujeme v kapitole [Routing]. Pojďme dál. Application už zná jméno presenteru a může pokračovat dál. Tím že vyrobí objekt třídy `ProductPresenter`, což je kód presenteru `Product`. Přesněji řečeno, požádá DI kontejner, aby presenter vyrobil, protože od vyrábění je tu on. @@ -100,11 +100,9 @@ Presenter může vypadat třeba takto: ```php class ProductPresenter extends Nette\Application\UI\Presenter { - private $repository; - - public function __construct(ProductRepository $repository) - { - $this->repository = $repository; + public function __construct( + private ProductRepository $repository, + ) { } public function renderShow(int $id): void @@ -123,21 +121,20 @@ Takže, zavolala se metoda `renderShow(123)`, jejíž kód je sice smyšlený p Následně presenter vrátí odpověď. Tou může být HTML stránka, obrázek, XML dokument, odeslání souboru z disku, JSON nebo třeba přesměrování na jinou stránku. Důležité je, že pokud explicitně neřekneme, jak má odpovědět (což je případ `ProductPresenter`), bude odpovědí vykreslení šablony s HTML stránkou. Proč? Protože v 99 % případů chceme vykreslit šablonu, tudíž presenter tohle chování bere jako výchozí a chce nám ulehčit práci. To je smyslem Nette. -Nemusíme ani uvádět, jakou šablonu vykreslit, cestu k ní si odvodí podle jednoduché logiky. V případě presenteru `Product` a akce `show` zkusí, zda existuje jeden z těchto souborů se šablonou uložených relativně od adresáře s třídou `ProductPresenter`: +Nemusíme ani uvádět, jakou šablonu vykreslit, cestu k ní si odvodí sám. V případě akce `show` jednodušše zkusí načíst šablonu `show.latte` v adresáři s třídou `ProductPresenter`. Taktéž se pokusí dohledat layout v souboru `@layout.latte` (podrobněji o [dohledávání šablon |templates#Hledání šablon]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` +A následně šablony vykreslí. Tím je úkol presenteru i celé aplikace dokonán a dílo jest završeno. Pokud by šablona neexistovala, vrátí se stránka s chybou 404. Více se o presenterech dočtete na stránce [Presentery|presenters]. -Taktéž se pokusí dohledat layout v souboru `@layout.latte` a následně šablonu vykreslí. Tím je úkol presenteru i celé aplikace dokonán a dílo jest završeno. Pokud by šablona neexistovala, vrátí se stránka s chybou 404. Více se o presenterech dočtete na stránce [Presentery|presenters]. +[* request-flow.svg *] Pro jistotu, zkusme si zrekapitulovat celý proces s trošku jinou URL: 1) URL bude `https://example.com` 2) bootujeme aplikaci, vytvoří se kontejner a spustí `Application::run()` -3) router URL dekóduje jako dvojici `Homepage:default` -4) vytvoří se objekt třídy `HomepagePresenter` +3) router URL dekóduje jako dvojici `Home:default` +4) vytvoří se objekt třídy `HomePresenter` 5) zavolá se metoda `renderDefault()` (pokud existuje) -6) vykreslí se šablona např. `templates/Homepage/default.latte` s layoutem např. `templates/@layout.latte` +6) vykreslí se šablona např. `default.latte` s layoutem např. `@layout.latte` Možná jste se teď setkali s velkou spoustou nových pojmů, ale věříme, že dávají smysl. Tvorba aplikací v Nette je ohromná pohodička. @@ -146,7 +143,7 @@ Možná jste se teď setkali s velkou spoustou nových pojmů, ale věříme, ž Šablony ======= -Když už přišla řeč na šablony, v Nette se používá šablonovací systém [Latte |latte:]. Proto taky ty koncovky `.latte` u šablon. Latte se používá jednak proto, že jde o nejlépe zabezpečený šablonovací systém pro PHP, a zároveň také systém nejintuitivnější. Nemusíte se učit mnoho nového, vystačíte si se znalostí PHP a několika značek. Všechno se dozvíte [v dokumentaci |latte:]. +Když už přišla řeč na šablony, v Nette se používá šablonovací systém [Latte |latte:]. Proto taky ty koncovky `.latte` u šablon. Latte se používá jednak proto, že jde o nejlépe zabezpečený šablonovací systém pro PHP, a zároveň také systém nejintuitivnější. Nemusíte se učit mnoho nového, vystačíte si se znalostí PHP a několika značek. Všechno se dozvíte [v dokumentaci |templates]. V šabloně se [vytvářejí odkazy |creating-links] na další presentery & akce takto: @@ -160,10 +157,7 @@ Prostě místo reálného URL napíšete známý pár `Presenter:action` a uvede detail produktu ``` -Generování URL má na starosti už dříve zmíněný router. Totiž routery v Nette jsou výjimečné tím, že dokáží provádět nejen transformace z URL na dvojici presenter:action, ale také obráceně, tedy z názvu presenteru + akce + parametrů vygenerovat URL. -Díky tomu v Nette můžete úplně změnit tvary URL v celé hotové aplikaci, aniž byste změnili jediný znak v šabloně nebo presenteru. Jen tím, že upravíte router. -Také díky tomu funguje tzv. kanonizace, což je další unikátní vlastnost Nette, která přispívá k lepšímu SEO (optimalizaci nalezitelnosti na internetu) tím, že automaticky zabraňuje existenci duplicitního obsahu na různých URL. -Hodně programátorů to považuje za ohromující. +Generování URL má na starosti už dříve zmíněný router. Totiž routery v Nette jsou výjimečné tím, že dokáží provádět nejen transformace z URL na dvojici presenter:action, ale také obráceně, tedy z názvu presenteru + akce + parametrů vygenerovat URL. Díky tomu v Nette můžete úplně změnit tvary URL v celé hotové aplikaci, aniž byste změnili jediný znak v šabloně nebo presenteru. Jen tím, že upravíte router. Také díky tomu funguje tzv. kanonizace, což je další unikátní vlastnost Nette, která přispívá k lepšímu SEO (optimalizaci nalezitelnosti na internetu) tím, že automaticky zabraňuje existenci duplicitního obsahu na různých URL. Hodně programátorů to považuje za ohromující. Interaktivní komponenty @@ -173,7 +167,7 @@ O presenterech vám musíme prozradit ještě jednu věc: mají v sobě zabudova Komponenty jsou samostatné znovupoužitelné celky, které vkládáme do stránek (tedy presenterů). Mohou to být [formuláře |forms:in-presenter], [datagridy |https://componette.org/contributte/datagrid/], menu, hlasovací ankety, vlastně cokoliv, co má smysl používat opakovaně. Můžeme vytvářet vlastní komponenty nebo používat některé z [ohromné nabídky |https://componette.org] open source komponent. -Komponenty zásadním způsobem ovlivňují přístup k tvorbě aplikacím. Otevřou vám nové možnosti skládání stránek z předpřipravených jednotek. A navíc mají něco společného s [Hollywoodem|components#Hollywood style]. +Komponenty zásadním způsobem ovlivňují přístup k tvorbě aplikacím. Otevřou vám nové možnosti skládání stránek z předpřipravených jednotek. A navíc mají něco společného s [Hollywoodem |components#Hollywood style]. DI kontejner a konfigurace @@ -185,9 +179,9 @@ Nemějte obavy, není to žádný magický black box, jak by se třeba mohlo z p Objektům, které DI kontejner vytváří, se z nějakého důvodu říká služby. -Co je na této třídě opravdu speciálního, tak že ji neprogramujete vy, ale framework. On skutečně vygeneruje PHP kód a uloží ho na disk. Vy jen dáváte instrukce, jaké objekty má umět kontejner vyrábět a jak přesně. A tyhle instrukce jsou zapsané v [konfiguračních souborech|bootstrap#konfigurace-di-kontejneru], pro které se používá formát [NEON|neon:format] a tedy mají i příponu `.neon`. +Co je na této třídě opravdu speciálního, tak že ji neprogramujete vy, ale framework. On skutečně vygeneruje PHP kód a uloží ho na disk. Vy jen dáváte instrukce, jaké objekty má umět kontejner vyrábět a jak přesně. A tyhle instrukce jsou zapsané v [konfiguračních souborech |bootstrapping#Konfigurace DI kontejneru], pro které se používá formát [NEON|neon:format] a tedy mají i příponu `.neon`. -Konfigurační soubory slouží čistě k instruování DI kontejneru. Takže když například uvedu v sekci [session|http:configuration#Session] volbu `expiration: 14 days`, tak DI kontejner při vytváření objektu `Nette\Http\Session` reprezentujícího session zavolá jeho metodu `setExpiration('14 days')` a tím se konfigurace stane realitou. +Konfigurační soubory slouží čistě k instruování DI kontejneru. Takže když například uvedu v sekci [session |http:configuration#Session] volbu `expiration: 14 days`, tak DI kontejner při vytváření objektu `Nette\Http\Session` reprezentujícího session zavolá jeho metodu `setExpiration('14 days')` a tím se konfigurace stane realitou. Je tu pro vás připravená celá kapitola popisující, co vše lze [konfigurovat |nette:configuring] a jak [definovat vlastní služby |dependency-injection:services]. @@ -197,7 +191,7 @@ Jakmile do vytváření služeb trošku proniknete, narazíte na slovo [autowiri Kam dál? ======== -Prošli jsme si základní principy aplikací v Nette. Zatím velmi povrchně, ale brzy proniknete do hloubky a časem vytvoříte báječné webové aplikace. Kam pokračovat dál? Vyzkoušeli jste si už tutoriál [Píšeme první aplikaci|quickstart:getting-started]? +Prošli jsme si základní principy aplikací v Nette. Zatím velmi povrchně, ale brzy proniknete do hloubky a časem vytvoříte báječné webové aplikace. Kam pokračovat dál? Vyzkoušeli jste si už tutoriál [Píšeme první aplikaci|quickstart:]? Kromě výše popsaného disponuje Nette celým arzenálem [užitečných tříd|utils:], [databázovou vrstvou|database:], atd. Zkuste si schválně jen tak proklikat dokumentaci. Nebo [blog|https://blog.nette.org]. Objevíte spoustu zajímavého. diff --git a/application/cs/modules.texy b/application/cs/modules.texy deleted file mode 100644 index d1158b7d2a..0000000000 --- a/application/cs/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Moduly -****** - -.[perex] -Moduly představují v Nette logické celky, ze kterých se aplikace skládá. Jejich součástí jsou presentery, šablony, případně i komponenty a modelové třídy. - -S jednou složkou pro presentery a jednou pro šablony bychom si u reálných projektů nevystačili. Mít v jedné složce desítky souborů je minimálně nepřehledné. Jak z toho ven? Jednoduše je na disku rozdělíme do podadresářů a v kódu do jmenných prostorů. A přesně to jsou v Nette moduly. - -Zapomeňme tedy na jednu složku pro presentery a šablony a místo toho vytvoříme moduly, například `Admin` a `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← adresář s moduly -│ ├── Admin/ ← modul Admin -│ │ ├── Presenters/ ← jeho presentery -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← modul Front -│ └── Presenters/ ← jeho presentery -│ └── ... -\-- - -Tuto adresářovou strukturu budou reflektovat jmenné prostory tříd, takže třeba `DashboardPresenter` bude v prostoru `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Na presenter `Dashboard` uvnitř modulu `Admin` se v rámci aplikace odkazujeme pomocí dvojtečkové notace jako na `Admin:Dashboard`, na jeho akci `default` potom jako na `Admin:Dashboard:default`. -A jak Nette vlastní ví, že `Admin:Dashboard` představuje třídu `App\Modules\Admin\Presenters\DashboardPresenter`? To mu řekneme pomocí [#mapování] v konfiguraci. -Tedy uvedená struktura není pevná a můžete si ji upravit podle potřeb. - -Moduly mohou kromě presenterů a šablon samozřejmě obsahovat všechny další součásti, jako jsou třeba komponenty, modelové třídy, atd. - - -Vnořené moduly --------------- - -Moduly nemusí tvořit jen plochou strukturu, lze vytvářet i submoduly, například: - -/--pre -app/ -├── Modules/ ← adresář s moduly -│ ├── Blog/ ← modul Blog -│ │ ├── Admin/ ← submodul Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodul Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← modul Forum -│ │ └── ... -\-- - -Tedy modul `Blog` je rozdělen do submodulů `Admin` a `Front`. A opět se to odrazí na jmenných prostorech, které budou `App\Modules\Blog\Admin\Presenters` apod. Na presenter `Dashboard` uvnitř submodulu se odkazujeme jako `Blog:Admin:Dashboard`. - -Zanořování může pokračovat libovolně hluboko, lze tedy vytvářet sub-submoduly. - - -Vytváření odkazů ----------------- - -Odkazy v šablonách presenterů jsou relativní vůči aktuálnímu modulu. Tedy odkaz `Foo:default` vede na presenter `Foo` v tomtéž modulu, v jakém je aktuální presenter. Pokud je aktuální modul například `Front`, pak odkaz vede takto: - -```latte -odkaz na Front:Product:show -``` - -Odkaz je relativní i pokud je jeho součástí název modulu, ten se pak považuje za submodul: - -```latte -odkaz na Front:Shop:Product:show -``` - -Absolutní odkazy zapisujeme analogicky k absolutním cestám na disku, jen místo lomítek jsou dvojtečky. Tedy absolutní odkaz začíná dvojtečkou: - -```latte -odkaz na Admin:Product:show -``` - -Pro zjištění, zda jsme v určitém modulu nebo jeho submodulu, použijeme funkci `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Routování ---------- - -Viz [kapitola o routování |routing#Moduly]. - - -Mapování --------- - -Definuje pravidla, podle kterých se z názvu presenteru odvodí název třídy. Zapisujeme je v [konfiguraci|configuration] pod klíčem `application › mapping`. - -Začněme ukázkou, která moduly nepoužívá. Budeme jen chtít, aby třídy presenterů měly jmenný prostor `App\Presenters`. Tedy aby se presenter například `Homepage` mapoval na třídu `App\Presenters\HomepagePresenter`. Toho lze docílit následující konfigurací: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Název presenteru se nahradí za hvezdičku v masce třídy a výsledkem je název třídy. Snadné! - -Pokud presentery členíme do modulů, můžeme pro každý modul mít vlastní mapování: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Nyní se presenter `Front:Homepage` mapuje na třídu `App\Modules\Front\Presenters\HomepagePresenter` a presenter `Admin:Dashboard` na třídu `App\Modules\Admin\Presenters\DashboardPresenter`. - -Praktičtější bude vytvořit obecné (hvězdičkové) pravidlo, které první dvě nahradí. V masce třídy přibude hvezdička navíc právě pro modul: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Ale co když používáme vícenásobně zanořené moduly a máme třeba presenter `Admin:User:Edit`? V takovém případě se segment s hvězdičkou představující modul pro každou úroveň jednoduše zopakuje a výsledkem bude třída `App\Modules\Admin\User\Presenters\EditPresenter`. - -Alternativním zápisem je místo řetězce použít pole skládající se ze tří segmentů. Tento zápis je ekvivaletní s předchozím: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Výchozí hodnotou je `*: *Module\*Presenter`. diff --git a/application/cs/multiplier.texy b/application/cs/multiplier.texy index 864e157001..4bd4c80f76 100644 --- a/application/cs/multiplier.texy +++ b/application/cs/multiplier.texy @@ -1,13 +1,15 @@ Multiplier: dynamické komponenty ******************************** -Nástroj na dynamickou tvorbu interaktivních komponent .[perex] +.[perex] +Nástroj na dynamickou tvorbu interaktivních komponent Vyjděme od typického příkladu: mějme seznam zboží v eshopu, přičemž u každého budeme chtít vypsat formulář pro přidání zboží do košíku. Jednou z možných variant je obalit celý výpis do jednoho formuláře. Mnohem pohodlnější způsob nám však nabízí [api:Nette\Application\UI\Multiplier]. Multiplier umožňuje pohodlně definovat továrničku pro více komponent. Funguje na principu vnořených komponent - každá komponenta dědící od [api:Nette\ComponentModel\Container] může obsahovat další komponenty. -Viz kapitola o [komponentovém modelu|components#komponenty-do-hloubky] v dokumentaci či [přednáška od Honzy Tvrdíka|https://www.youtube.com/watch?v=8y3LLexWu-I]. .[tip] +.[tip] +Viz kapitola o [komponentovém modelu |components#Komponenty do hloubky] v dokumentaci či [přednáška od Honzy Tvrdíka|https://www.youtube.com/watch?v=8y3LLexWu-I]. Podstatou Multiplieru je, že vystupuje v pozici rodiče, který si své potomky dokáže vytvářet dynamicky pomocí callbacku předaného v konstruktoru. Viz příklad: diff --git a/application/cs/presenters.texy b/application/cs/presenters.texy index 17df36e374..368fc7ceda 100644 --- a/application/cs/presenters.texy +++ b/application/cs/presenters.texy @@ -11,7 +11,7 @@ Seznámíme se s tím, jak se v Nette píší presentery a šablony. Po přečte
    -[Už víme |how-it-works#nette-application], že presenter je třída, která představuje nějakou konkrétní stránku webové aplikace, např. homepage; produkt v e-shopu; přihlašovací formulář; sitemap feed atd. Aplikace může mít od jednoho po tisíce presenterů. V jiných frameworcích se jim také říká controllery. +[Už víme |how-it-works#Nette Application], že presenter je třída, která představuje nějakou konkrétní stránku webové aplikace, např. homepage; produkt v e-shopu; přihlašovací formulář; sitemap feed atd. Aplikace může mít od jednoho po tisíce presenterů. V jiných frameworcích se jim také říká controllery. Obvykle se pod pojmem presenter myslí potomek třídy [api:Nette\Application\UI\Presenter], který je vhodný pro generování webových rozhraní a kterému se budeme věnovat ve zbytku této kapitoly. V obecném smyslu je presenter jakýkoliv objekt implementující rozhraní [api:Nette\Application\IPresenter]. @@ -39,12 +39,9 @@ Presenter by neměl obstarávat byznys logiku aplikace, zapisovat a číst z dat ```php class ArticlePresenter extends Nette\Application\UI\Presenter { - /** @var ArticleRepository */ - private $articles; - - public function __construct(ArticleRepository $articles) - { - $this->articles = $articles; + public function __construct( + private ArticleRepository $articles, + ) { } } ``` @@ -59,11 +56,11 @@ Ihned po obdržení požadavku se zavolá metoda `startup()`. Můžete ji využ `action(args...)` .{toc: action()} -------------------------------------------------- -Obdoba metody `render()`. Zatímco `render()` je určená k tomu, aby připravila data pro konkrétní šablonu, která se následně vykreslí, tak v `action()` se zpracovává požadavek bez návaznosti na vykreslování šablony. Například se zpracují data, přihlásí či odhlásí uživatel, a tak podobně, a poté [přesměruje jinam|#Přesměrování]. +Obdoba metody `render()`. Zatímco `render()` je určená k tomu, aby připravila data pro konkrétní šablonu, která se následně vykreslí, tak v `action()` se zpracovává požadavek bez návaznosti na vykreslování šablony. Například se zpracují data, přihlásí či odhlásí uživatel, a tak podobně, a poté [přesměruje jinam |#Přesměrování]. Důležité je, že `action()` se volá dříve než `render()`, takže v ní můžeme případně změnit další běh dějin, tj. změnit šablonu, která se bude kreslit, a také metodu `render()`, která se bude volat. A to pomocí `setView('jineView')`. -Metodě se předávají parametry z požadavku. Je možné a doporučené uvést parametrům typy, např. `actionShow(int $id, string $slug = null)` - pokud bude parametr `id` chybět nebo pokud nebude integer, presenter vrátí [chybu 404|#Chyba 404 a spol.] a ukončí činnost. +Metodě se předávají parametry z požadavku. Je možné a doporučené uvést parametrům typy, např. `actionShow(int $id, ?string $slug = null)` - pokud bude parametr `id` chybět nebo pokud nebude integer, presenter vrátí [chybu 404 |#Chyba 404 a spol] a ukončí činnost. `handle(args...)` .{toc: handle()} @@ -118,11 +115,11 @@ Odpovědí presenteru je zpravidla [vykreslení šablony s HTML stránkou|templa Kdykoliv během životního cyklu můžeme některou z následujících metod odeslat odpověď a zároveň tak ukončit presenter: -- `redirect()`, `redirectPermanent()`, `redirectUrl()` a `forward()` [přesměruje|#přesměrování] -- `error()` ukončí presenter [kvůli chybě|#Chyba 404 a spol.] +- `redirect()`, `redirectPermanent()`, `redirectUrl()` a `forward()` [přesměruje |#Přesměrování] +- `error()` ukončí presenter [kvůli chybě |#Chyba 404 a spol] - `sendJson($data)` presenter ukončí a [odešle data |#Odeslání JSON] ve formátu JSON - `sendTemplate()` presenter ukončí a ihned [vykreslí šablonu |templates] -- `sendResponse($response)` presenter ukončí a odešle [vlastní odpověď|#Odpovědi] +- `sendResponse($response)` presenter ukončí a odešle [vlastní odpověď |#Odpovědi] - `terminate()` presenter ukončí bez odpovědi Pokud žádnou z těchto metod nezavoláte, presenter automaticky přistoupí k vykreslí šablony. Proč? Protože v 99 % případů chceme vykreslit šablonu, tudíž presenter tohle chování bere jako výchozí a chce nám ulehčit práci. @@ -161,7 +158,7 @@ Metoda `forward()` přejde na nový presenter okamžitě bez HTTP přesměrován $this->forward('Product:show'); ``` -Příklad tzv. dočasného přesměrování s HTTP kódem 302 nebo 303: +Příklad tzv. dočasného přesměrování s HTTP kódem 302 (nebo 303, je-li metoda aktuálního požadavku POST): ```php $this->redirect('Product:show', $id); @@ -173,7 +170,7 @@ Permanentní přesměrování s HTTP kódem 301 docílíte takto: $this->redirectPermanent('Product:show', $id); ``` -Na jinou URL mimo aplikaci lze přesměrovat metodou `redirectUrl()`: +Na jinou URL mimo aplikaci lze přesměrovat metodou `redirectUrl()`. Jako druhý parametr lze uvést HTTP kód, výchozí je 302 (nebo 303, je-li metoda aktuálního požadavku POST): ```php $this->redirectUrl('https://nette.org'); @@ -181,7 +178,7 @@ $this->redirectUrl('https://nette.org'); Přesměrování okamžitě ukončí činnost presenteru vyhozením tzv. tiché ukončovací výjimky `Nette\Application\AbortException`. -Před přesměrováním lze odeslat [flash message |#flash zprávy], tedy zprávy, které budou po přesměrování zobrazeny v šabloně. +Před přesměrováním lze odeslat [flash message |#Flash zprávy], tedy zprávy, které budou po přesměrování zobrazeny v šabloně. Flash zprávy @@ -208,7 +205,7 @@ $this->redirect(/* ... */); // a přesměrujeme Chyba 404 a spol. ================= -Pokud nelze splnit požadavek, třeba z důvodu, že článek který chceme zobrazit neexistuje v databázi, vyhodíme chybu 404 metodou `error(string $message = null, int $httpCode = 404)`. +Pokud nelze splnit požadavek, třeba z důvodu, že článek který chceme zobrazit neexistuje v databázi, vyhodíme chybu 404 metodou `error(?string $message = null, int $httpCode = 404)`. ```php public function renderShow(int $id): void @@ -221,8 +218,7 @@ public function renderShow(int $id): void } ``` -HTTP kód chyby lze předat jako druhý parametr, výchozí je 404. Metoda funguje tak, že vyhodí výjimku `Nette\Application\BadRequestException`, načež `Application` předá řízení error-presenteru. Což je presenter, jehož úkolem je zobrazit stránku informující o nastalé chybě. -Nastavení error-preseteru se provádí v [konfiguraci application|configuration]. +HTTP kód chyby lze předat jako druhý parametr, výchozí je 404. Metoda funguje tak, že vyhodí výjimku `Nette\Application\BadRequestException`, načež `Application` předá řízení error-presenteru. Což je presenter, jehož úkolem je zobrazit stránku informující o nastalé chybě. Nastavení error-preseteru se provádí v [konfiguraci application|configuration]. Odeslání JSON @@ -239,49 +235,83 @@ public function actionData(): void ``` -Persistentní parametry -====================== +Parametry požadavku .{data-version:3.1.14} +========================================== -Persistentní parametry se v odkazech **přenášejí automaticky**. To znamená, že je nemusíme explicitně uvádět v každém volání `link()` nebo `n:href` v šabloně, ale přesto se přenesou. +Presenter a také každá komponenta získává z HTTP požadavku své parametry. Jejich hodnotu zjistíte metodou `getParameter($name)` nebo `getParameters()`. Hodnoty jsou řetězce či pole řetězců, jde v podstatě o surové data získané přímo z URL. -Pokud má vaše aplikace více jazykových mutací, tak aktuální jazyk je parameter, který musí být neustále součástí URL. A bylo by neskutečně únavné ho v každém odkazu uvádět. To není s Nette potřeba. Prostě si parametr `lang` označíme jako persistentní tímto způsobem: +Pro větší pohodlí doporučujeme parametry zpřístupnit přes property. Stačí je označit atributem `#[Parameter]`: ```php -class ProductPresenter extends Nette\Application\UI\Presenter +use Nette\Application\Attributes\Parameter; // tento řádek je důležitý + +class HomePresenter extends Nette\Application\UI\Presenter { - /** @persistent */ - public $lang; + #[Parameter] + public string $theme; // musí být public } ``` -Pokud aktuální hodnota parametru `lang` bude `'en'`, tak URL vytvořené pomocí `link()` nebo `n:href` v šabloně bude obsahovat `lang=en`. Paráda! +U property doporučujeme uvádět i datový typ (např. `string`) a Nette podle něj hodnotu automaticky přetypuje. Hodnoty parametrů lze také [validovat |#Validace parametrů]. -Při vytváření odkazu nicméně lze persistentní parametr uvést a tím změnit jeho hodnotu: +Při vytváření odkazu lze parametrům hodnotu přímo nastavit: ```latte -detail v češtině +click ``` -Nebo jej lze naopak odstranit tím, že ho vynulujeme: -```latte -klikni -``` +Persistentní parametry +====================== -Persistentní proměnná musí být deklarovaná jako public. Můžeme uvést i výchozí hodnotu. Bude-li mít parametr hodnotu stejnou jako výchozí, nebude v URL obsažen. +Persistentní parametry slouží k udržování stavu mezi různými požadavky. Jejich hodnota zůstává stejná i po kliknutí na odkaz. Na rozdíl od dat v session se přenášejí v URL. A to zcela automaticky, není tedy potřeba je explicitně uvádět v `link()` nebo `n:href`. -Persistence zohledňuje hierarchii tříd presenterů, tedy parametr definovaný v určitém presenteru nebo traitě je poté automaticky přenášen do každého presenteru z něj dědícího nebo užívajícího stejnou traitu. +Příklad použití? Máte multijazyčnou aplikaci. Aktuální jazyk je parameter, který musí být neustále součástí URL. Ale bylo by neskutečně únavné ho v každém odkazu uvádět. Tak z něj uděláte persistentní parametr `lang` a bude se přenášet sám. Paráda! -V PHP 8 můžete pro označení persistentních parametrů použít také atributy: +Vytvoření persistentního parametru je v Nette nesmírně jednoduché. Stačí vytvořit veřejnou property a označit ji atributem: (dříve se používalo `/** @persistent */`) ```php -use Nette\Application\Attributes\Persistent; +use Nette\Application\Attributes\Persistent; // tento řádek je důležitý class ProductPresenter extends Nette\Application\UI\Presenter { #[Persistent] - public $lang; + public string $lang; // musí být public +} +``` + +Pokud bude `$this->lang` mít hodnotu například `'en'`, tak i odkazy vytvořené pomocí `link()` nebo `n:href` budou obsahovat parameter `lang=en`. A po kliknutí na odkaz bude opět `$this->lang = 'en'`. + +U property doporučujeme uvádět i datový typ (např. `string`) a můžete uvést i výchozí hodnotu. Hodnoty parametrů lze [validovat |#Validace parametrů]. + +Persistentní parametry se standardně přenášejí mezi všemi akcemi daného presenteru. Aby se přenášely i mezi více presentery, je potřeba je definovat buď: + +- ve společném předkovi, od kterého presentery dědí +- v traitě, kterou presentery použijí: + +```php +trait LanguageAware +{ + #[Persistent] + public string $lang; } + +class ProductPresenter extends Nette\Application\UI\Presenter +{ + use LanguageAware; +} +``` + +Při vytváření odkazu lze persistentnímu parametru změnit hodnotu: + +```latte +detail v češtině +``` + +Nebo jej lze *vyresetovat*, tj. odstranit z URL. Pak bude nabývat svou výchozí hodnotu: + +```latte +klikni ``` @@ -302,15 +332,36 @@ Jdeme do hloubky S tím, co jsme si dosud v této kapitole ukázali, si nejspíš úplně vystačíte. Následující řádky jsou určeny těm, kdo se zajímají o presentery do hloubky a chtějí vědět úplně všechno. -Požadavek a parametry ---------------------- +Validace parametrů +------------------ -Požadavek, který vyřizuje presenter, je objekt [api:Nette\Application\Request] a vrací ho metoda presenteru `getRequest()`. Jeho součástí je pole parametrů a každý z nich patří buď některé z komponent, nebo přímo presenteru (což je vlastně také komponenta, byť speciální). Nette tedy parametry přerozdělí a předá mezi jednotlivé komponenty (a presenter) zavoláním metody `loadState(array $params)`, což dále popisujeme v kapitole [Komponenty|components]. Získat parametry lze metodu `getParameters(): array`, jednotlivě pomocí `getParameter($name)`. Hodnoty parametrů jsou řetězce či pole řetězců, jde v podstatě o surové data získané přímo z URL. +Hodnoty [parametrů požadavku |#Parametry požadavku] a [persistentních parametrů |#Persistentní parametry] přijatých z URL zapisuje do properties metoda `loadState()`. Ta také kontroluje, zda odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí. + +Nikdy slepě nevěřte parametrům, protože mohou být snadno uživatelem přepsány v URL. Takto například ověříme, zda je jazyk `$this->lang` mezi podporovanými. Vhodnou cestou je přepsat zmíněnou metodu `loadState()`: + +```php +class ProductPresenter extends Nette\Application\UI\Presenter +{ + #[Persistent] + public string $lang; + + public function loadState(array $params): void + { + parent::loadState($params); // zde se nastaví $this->lang + // následuje vlastní kontrola hodnoty: + if (!in_array($this->lang, ['en', 'cs'])) { + $this->error(); + } + } +} +``` Uložení a obnovení požadavku ---------------------------- +Požadavek, který vyřizuje presenter, je objekt [api:Nette\Application\Request] a vrací ho metoda presenteru `getRequest()`. + Aktuální požadavek lze uložit do session nebo naopak z ní obnovit a nechat jej presenter znovu vykonat. To se hodí například v situaci, když uživatel vyplňuje formulář a vyprší mu přihlášení. Aby o data nepřišel, před přesměrováním na přihlašovací stránku aktuální požadavek uložíme do session pomocí `$reqId = $this->storeRequest()`, které vrátí jeho identifikátor v podobě krátkého řetězce a ten předáme jako parameter přihlašovacímu presenteru. Po přihlášení zavoláme metodu `$this->restoreRequest($reqId)`, která požadavek vyzvedne ze session a forwarduje na něj. Metoda přitom ověří, že požadavek vytvořil stejný uživatel, jako se nyní přihlásil. Pokud by se přihlásil jiný uživatel nebo klíč byl neplatný, neudělá nic a program pokračuje dál. @@ -332,7 +383,7 @@ K přesměrování nedojde při AJAXovém nebo POST požadavku, protože by doš Kanonizaci můžete vyvolat i manuálně pomocí metody `canonicalize()`, které se podobně jako metodě `link()` předá presenter, akce a parametry. Vyrobí odkaz a porovná ho s aktuální URL adresou. Pokud se liší, tak přesměruje na vygenerovaný odkaz. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // přesměruje, pokud $slug se liší od $realSlug @@ -344,7 +395,7 @@ public function actionShow(int $id, string $slug = null): void Události -------- -Kromě metod `startup()`, `beforeRender()` a `shutdown()`, které se volají jako součást životního cyklu presenteru, lze definovat ještě další funkce, které se mají automaticky zavolat. Presenter definuje tzv. [událost|nette:glossary#Události], jejichž handlery přidáte do polí `$onStartup`, `$onRender` a `$onShutdown`. +Kromě metod `startup()`, `beforeRender()` a `shutdown()`, které se volají jako součást životního cyklu presenteru, lze definovat ještě další funkce, které se mají automaticky zavolat. Presenter definuje tzv. [událost |nette:glossary#události], jejichž handlery přidáte do polí `$onStartup`, `$onRender` a `$onShutdown`. ```php class ArticlePresenter extends Nette\Application\UI\Presenter @@ -393,3 +444,57 @@ $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $ht }; $this->sendResponse(new Responses\CallbackResponse($callback)); ``` + + +Omezení přístupu pomocí `#[Requires]` .{data-version:3.2.2} +----------------------------------------------------------- + +Atribut `#[Requires]` poskytuje pokročilé možnosti pro omezení přístupu k presenterům a jejich metodám. Lze jej použít pro specifikaci HTTP metod, vyžadování AJAXového požadavku, omezení na stejný původ (same origin), a přístup pouze přes forwardování. Atribut lze aplikovat jak na třídy presenterů, tak na jednotlivé metody `action()`, `render()`, `handle()` a `createComponent()`. + +Můžete určit tyto omezení: +- na HTTP metody: `#[Requires(methods: ['GET', 'POST'])]` +- vyžadování AJAXového požadavku: `#[Requires(ajax: true)]` +- přístup pouze ze stejného původu: `#[Requires(sameOrigin: true)]` +- přístup pouze přes forward: `#[Requires(forward: true)]` +- omezení na konkrétní akce: `#[Requires(actions: 'default')]` + +Podrobnosti najdete v návodu [Jak používat atribut Requires |best-practices:attribute-requires]. + + +Kontrola HTTP metody +-------------------- + +Presentery v Nette automaticky ověřují HTTP metodu každého příchozího požadavku. Důvodem pro tuto kontrolu je především bezpečnost. Standardně jsou povoleny metody `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. + +Chcete-li povolit navíc například metodu `OPTIONS`, použijte k tomu atribut `#[Requires]` (od Nette Application v3.2): + +```php +#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] +class MyPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Ve verzi 3.1 se ověření provádí v `checkHttpMethod()`, která zjišťuje, zda je metoda specifikovaná v požadavku obsažena v poli `$presenter->allowedMethods`. Přidání metody udělejte takto: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +Je důležité zdůraznit, že pokud povolíte metodu `OPTIONS`, musíte ji následně také patřičně obsloužit v rámci svého presenteru. Metoda je často používána jako tzv. preflight request, který prohlížeč automaticky odesílá před skutečným požadavkem, když je potřeba zjistit, zda je požadavek povolený z hlediska CORS (Cross-Origin Resource Sharing) politiky. Pokud metodu povolíte, ale neimplementujete správnou odpověď, může to vést k nekonzistencím a potenciálním bezpečnostním problémům. + + +Další četba +=========== + +- [Metody a atributy inject |best-practices:inject-method-attribute] +- [Skládání presenterů z trait |best-practices:presenter-traits] +- [Předání nastavení do presenterů |best-practices:passing-settings-to-presenters] +- [Jak se vrátit k dřívější stránce |best-practices:restore-request] diff --git a/application/cs/routing.texy b/application/cs/routing.texy index ee59df11a1..826bed4363 100644 --- a/application/cs/routing.texy +++ b/application/cs/routing.texy @@ -12,10 +12,9 @@ Router má na starosti vše okolo URL adres, aby vy už jste nad nimi nemuseli p
    -Lidštější URL (nebo taky cool či pretty URL) jsou použitelnější, zapamatovatelnější a pozitivně přispívají k SEO. Nette na to myslí a vychází vývojářům plně vstříc. Můžete si pro svou aplikaci navrhnout přesně takovou strukturu URL adres, jakou budete chtít. -Můžete ji navrhnout dokonce až ve chvíli, když už je aplikace hotová, protože se to obejde bez zásahů do kódu či šablon. Definuje se totiž elegantním způsobem na jednom [jediném místě |#Začlenění do aplikace], v routeru, a není tak roztroušen ve formě anotací ve všech presenterech. +Lidštější URL (nebo taky cool či pretty URL) jsou použitelnější, zapamatovatelnější a pozitivně přispívají k SEO. Nette na to myslí a vychází vývojářům plně vstříc. Můžete si pro svou aplikaci navrhnout přesně takovou strukturu URL adres, jakou budete chtít. Můžete ji navrhnout dokonce až ve chvíli, když už je aplikace hotová, protože se to obejde bez zásahů do kódu či šablon. Definuje se totiž elegantním způsobem na jednom [jediném místě |#Začlenění do aplikace], v routeru, a není tak roztroušen ve formě anotací ve všech presenterech. -Router v Nette je mimořádný tím, že je **obousměrný.** Umí jak dekódovat URL v HTTP požadavku, tak i odkazy vytvářet. Hraje tedy zásadní roli v [Nette Application|how-it-works#Nette Application], protože jednak rozhoduje o tom, který presenter a action bude vykonávat aktuální požadavek, ale také se využívá pro [generování URL |creating-links] v šabloně atd. +Router v Nette je mimořádný tím, že je **obousměrný.** Umí jak dekódovat URL v HTTP požadavku, tak i odkazy vytvářet. Hraje tedy zásadní roli v [Nette Application |how-it-works#Nette Application], protože jednak rozhoduje o tom, který presenter a action bude vykonávat aktuální požadavek, ale také se využívá pro [generování URL |creating-links] v šabloně atd. Ovšem router není limitován jen pro tohle využití, můžete jej použít v aplikacích, kde se vůbec presentery nepoužívají, pro REST API, atd. Více v části [#samostatné použití]. @@ -34,9 +33,6 @@ $router->addRoute('article/', 'Article:view'); Ukázka říká, že pokud v prohlížeči otevřeme `https://domain.com/rss.xml`, zobrazí se presenter `Feed` s akcí `rss`, pokud `https://domain.com/article/12`, zobrazí se presenter `Article` s akcí `view` atd. V případě nenalezení vhodné routy reaguje Nette Application vyhozením výjimky [BadRequestException |api:Nette\Application\BadRequestException], která se uživateli zobrazí jako chybová stránka 404 Not Found. -.[note] -V Nette 2.x se místo `$router->addRoute(...)` používalo `$router[] = new Route(...)`. - Pořadí rout ----------- @@ -96,12 +92,12 @@ Routa bude nyní akceptovat i URL `https://example.com/chronicle/`, které opět Parametrem může být samozřejmě i jméno presenteru a akce. Třeba takto: ```php -$router->addRoute('/', 'Homepage:default'); +$router->addRoute('/', 'Home:default'); ``` Uvedená routa akceptuje např. URL ve tvaru `/article/edit` nebo také `/catalog/list` a chápe je jako presentery a akce `Article:edit` a `Catalog:list`. -Zaroveň dává parametrům `presenter` a `action` výchozí hodnoty `Homepage` a `default` a jsou tedy také volitelné. Takže routa akceptuje i URL ve tvaru `/article` a chápe ji jako `Article:default`. Nebo obráceně, odkaz na `Product:default` vygeneruje cestu `/product`, odkaz na výchozí `Homepage:default` cestu `/`. +Zaroveň dává parametrům `presenter` a `action` výchozí hodnoty `Home` a `default` a jsou tedy také volitelné. Takže routa akceptuje i URL ve tvaru `/article` a chápe ji jako `Article:default`. Nebo obráceně, odkaz na `Product:default` vygeneruje cestu `/product`, odkaz na výchozí `Home:default` cestu `/`. Maska může popisovat nejen relativní cestu od kořenového adresáře webu, ale také absolutní cestu, pokud začíná lomítkem, nebo dokonce celé absolutní URL, začíná-li dvěma lomítky: @@ -163,7 +159,7 @@ Sekvence je možné libovolně zanořovat a kombinovat: ```php $router->addRoute( '[[-]/][/page-]', - 'Homepage:default' + 'Home:default', ); // Akceptuje cesty: @@ -186,16 +182,16 @@ $router->addRoute('[!.html]', /* ... */); Volitelné parametry (tj. parametry mající výchozí hodnotu) bez hranatých závorek se chovají v podstatě tak, jako by byly uzávorkovány následujícím způsobem: ```php -$router->addRoute('//', /* ... */); +$router->addRoute('//', /* ... */); // odpovídá tomuto: -$router->addRoute('[/[/[]]]', /* ... */); +$router->addRoute('[/[/[]]]', /* ... */); ``` -Pokud bychom chtěli ovlivnit chování koncového lomítka, aby se např. místo `/homepage/` generovalo jen `/homepage`, lze toho docílit takto: +Pokud bychom chtěli ovlivnit chování koncového lomítka, aby se např. místo `/home/` generovalo jen `/home`, lze toho docílit takto: ```php -$router->addRoute('[[/[/]]]', /* ... */); +$router->addRoute('[[/[/]]]', /* ... */); ``` @@ -219,34 +215,34 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ - 'presenter' => 'Homepage', + 'presenter' => 'Home', 'action' => 'default', ]); ``` -Nebo můžeme použít tuto formu, všimněte si přepisu validačního regulárního výrazu: +Pro detailnější specifikaci lze použít ještě rozšířenější formu, kde kromě výchozích hodnot můžeme nastavit i další vlastnosti parametrů, jako třeba validační regulární výraz (viz parametr `id`): ```php use Nette\Routing\Route; $router->addRoute('/[/]', [ 'presenter' => [ - Route::VALUE => 'Homepage', + Route::Value => 'Home', ], 'action' => [ - Route::VALUE => 'default', + Route::Value => 'default', ], 'id' => [ - Route::PATTERN => '\d+', + Route::Pattern => '\d+', ], ]); ``` -Tyto upovídanější formáty se hodí pro doplnění dalších metadat. +Je důležité poznamenat, že pokud parametry definované v poli nejsou uvedeny v masce cesty, jejich hodnoty nelze změnit, ani pomocí query parametrů uvedených za otazníkem v URL. Filtry a překlady @@ -255,7 +251,7 @@ Filtry a překlady Zdrojové kódy aplikace píšeme v angličtině, ale pokud má mít web české URL, pak jednoduché routování typu: ```php -$router->addRoute('/', 'Homepage:default'); +$router->addRoute('/', 'Home:default'); ``` bude generovat anglické URL, jako třeba `/product/123` nebo `/cart`. Pokud chceme mít presentery a akce v URL reprezentované českými slovy (např. `/produkt/123` nebo `/kosik`), můžeme využít překladového slovníku. Pro jeho zápis už potřebujeme "upovídanější" variantu druhého parametru: @@ -265,8 +261,8 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => [ - Route::VALUE => 'Homepage', - Route::FILTER_TABLE => [ + Route::Value => 'Home', + Route::FilterTable => [ // řetězec v URL => presenter 'produkt' => 'Product', 'kosik' => 'Cart', @@ -274,8 +270,8 @@ $router->addRoute('/', [ ], ], 'action' => [ - Route::VALUE => 'default', - Route::FILTER_TABLE => [ + Route::Value => 'default', + Route::FilterTable => [ 'seznam' => 'list', ], ], @@ -284,7 +280,7 @@ $router->addRoute('/', [ Více klíčů překladového slovníku může vést na tentýž presenter. Tím se k němu vytvoří různé aliasy. Za kanonickou variantu (tedy tu, která bude ve vygenerovaném URL) se považuje poslední klíč. -Překladovou tabulku lze tímto způsobem použít na jakýkoliv parametr. Přičemž pokud překlad neexistuje, bere se původní hodnota. Tohle chování můžeme změnit doplněním `Route::FILTER_STRICT => true` a routa pak odmítne URL, pokud hodnota není ve slovníku. +Překladovou tabulku lze tímto způsobem použít na jakýkoliv parametr. Přičemž pokud překlad neexistuje, bere se původní hodnota. Tohle chování můžeme změnit doplněním `Route::FilterStrict => true` a routa pak odmítne URL, pokud hodnota není ve slovníku. Kromě překladového slovníku v podobě pole lze nasadit i vlastní překladové funkce. @@ -293,16 +289,16 @@ use Nette\Routing\Route; $router->addRoute('//', [ 'presenter' => [ - Route::VALUE => 'Homepage', - Route::FILTER_IN => function (string $s): string { /* ... */ }, - Route::FILTER_OUT => function (string $s): string { /* ... */ }, + Route::Value => 'Home', + Route::FilterIn => function (string $s): string { /* ... */ }, + Route::FilterOut => function (string $s): string { /* ... */ }, ], 'action' => 'default', 'id' => null, ]); ``` -Funkce `Route::FILTER_IN` převádí mezi parametrem v URL a řetězcem, který se poté předá do presenteru, funkce `FILTER_OUT` zajišťuje převod opačným směrem. +Funkce `Route::FilterIn` převádí mezi parametrem v URL a řetězcem, který se poté předá do presenteru, funkce `FilterOut` zajišťuje převod opačným směrem. Parametry `presenter`, `action` a `module` už mají předdefinované filtry, které převádějí mezi stylem PascalCase resp. camelCase a kebab-case používaným v URL. Výchozí hodnota parametrů se zapisuje už v transformované podobě, takže třeba v případě presenteru píšeme ``, nikoliv ``. @@ -316,24 +312,24 @@ Vedle filtrů určených pro konkrétní parametry můžeme definovat též obec use Nette\Routing\Route; $router->addRoute('/', [ - 'presenter' => 'Homepage', + 'presenter' => 'Home', 'action' => 'default', null => [ - Route::FILTER_IN => function (array $params): array { /* ... */ }, - Route::FILTER_OUT => function (array $params): array { /* ... */ }, + Route::FilterIn => function (array $params): array { /* ... */ }, + Route::FilterOut => function (array $params): array { /* ... */ }, ], ]); ``` Obecné filtry dávají možnost upravit chování routy naprosto jakýmkoliv způsobem. Můžeme je použít třeba pro modifikaci parametrů na základě jiných parametrů. Například přeložení `` a `` na základě aktuální hodnoty parametru ``. -Pokud má parametr definovaný vlastní filtr a současně existuje obecný filtr, provede se vlastní `FILTER_IN` před obecným a naopak obecný `FILTER_OUT` před vlastním. Tedy uvnitř obecného filtru jsou hodnoty parametrů `presenter` resp. `action` zapsané ve stylu PascalCase resp. camelCase. +Pokud má parametr definovaný vlastní filtr a současně existuje obecný filtr, provede se vlastní `FilterIn` před obecným a naopak obecný `FilterOut` před vlastním. Tedy uvnitř obecného filtru jsou hodnoty parametrů `presenter` resp. `action` zapsané ve stylu PascalCase resp. camelCase. -Jednosměrky ONE_WAY -------------------- +Jednosměrky OneWay +------------------ -Jednosměrné routy se používají pro zachování funkčnosti starých URL, které už aplikace negeneruje, ale stále přijímá. Označíme je příznakem `ONE_WAY`: +Jednosměrné routy se používají pro zachování funkčnosti starých URL, které už aplikace negeneruje, ale stále přijímá. Označíme je příznakem `OneWay`: ```php // staré URL /product-info?id=123 @@ -345,10 +341,33 @@ $router->addRoute('product/', 'Product:detail'); Při přístupu na starou URL presenter automaticky přesměruje na nové URL, takže vám tyto stránky vyhledávače nezaindexují dvakrát (viz [#SEO a kanonizace]). +Dynamické routování s callbacky +------------------------------- + +Dynamické routování s callbacky vám umožňuje přiřadit routám přímo funkce (callbacky), které se vykonají, když je daná cesta navštívena. Tato flexibilní funkčnost vám umožní rychle a efektivně vytvářet různé koncové body (endpoints) pro vaši aplikaci: + +```php +$router->addRoute('test', function () { + echo 'jste na adrese /test'; +}); +``` + +Můžete také definovat v masce parametry, které se automaticky předají do vašeho callbacku: + +```php +$router->addRoute('', function (string $lang) { + echo match ($lang) { + 'cs' => 'Vítejte na české verzi našeho webu!', + 'en' => 'Welcome to the English version of our website!', + }; +}); +``` + + Moduly ------ -Pokud máme více rout, které spadají do společného [modulu |modules], využijeme `withModule()`: +Pokud máme více rout, které spadají do společného [modulu |directory-structure#Presentery a šablony], využijeme `withModule()`: ```php $router = new RouteList; @@ -365,7 +384,7 @@ Alternativou je použití parametru `module`: ```php // URL manage/dashboard/default se mapuje na presenter Admin:Dashboard $router->addRoute('manage//', [ - 'module' => 'Admin' + 'module' => 'Admin', ]); ``` @@ -457,10 +476,10 @@ $router->addRoute('index', /* ... */); Začlenění do aplikace ===================== -Abychom vytvořený router zapojili do aplikace, musíme o něm říci DI kontejneru. Nejsnazší cesta je připravit továrnu, která objekt routeru vyrobí, a sdělit v konfiguraci kontejneru, že ji má použít. Dejme tomu, že k tomu účelu napíšeme metodu `App\Router\RouterFactory::createRouter()`: +Abychom vytvořený router zapojili do aplikace, musíme o něm říci DI kontejneru. Nejsnazší cesta je připravit továrnu, která objekt routeru vyrobí, a sdělit v konfiguraci kontejneru, že ji má použít. Dejme tomu, že k tomu účelu napíšeme metodu `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -479,7 +498,7 @@ Do [konfigurace |dependency-injection:services] pak zapíšeme: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Jakékoliv závislosti, třeba na databázi atd, se předají tovární metodě jako její parametry pomocí [autowiringu|dependency-injection:autowiring]: @@ -506,15 +525,15 @@ http://example.com/?presenter=Product&action=detail&id=123 Parametrem konstruktoru SimpleRouteru je výchozí presenter & akce, na který se má směřovat, pokud otevřeme stránku bez parametrů, např. `http://example.com/`. ```php -// výchozím presenterem bude 'Homepage' a akce 'default' -$router = new Nette\Application\Routers\SimpleRouter('Homepage:default'); +// výchozím presenterem bude 'Home' a akce 'default' +$router = new Nette\Application\Routers\SimpleRouter('Home:default'); ``` Doporučujeme SimpleRouter přímo definovat v [konfiguraci |dependency-injection:services]: ```neon services: - - Nette\Application\Routers\SimpleRouter('Homepage:default') + - Nette\Application\Routers\SimpleRouter('Home:default') ``` @@ -523,9 +542,9 @@ SEO a kanonizace Framework přispívá k SEO (optimalizaci nalezitelnosti na internetu) tím, že zabraňuje duplicitě obsahu na různých URL. Pokud k určitému cíli vede více adres, např. `/index` a `/index.html`, framework první z nich určí za primární (kanonickou) a ostatní na ni přesměruje pomocí HTTP kódu 301. Díky tomu vám vyhledávače stránky nezaindexují dvakrát a nerozmělní jejich page rank. -Tomuto procesu se říká kanonizace. Kanonickou URL je ta, kterou vygeneruje router, tj. první vyhovující routa v kolekci bez příznaku ONE_WAY. Proto v kolekci uvádíme **primární routy jako první**. +Tomuto procesu se říká kanonizace. Kanonickou URL je ta, kterou vygeneruje router, tj. první vyhovující routa v kolekci bez příznaku OneWay. Proto v kolekci uvádíme **primární routy jako první**. -Kanonizaci provádí presenter, více v kapitole [kanonizace|presenters#kanonizace]. +Kanonizaci provádí presenter, více v kapitole [kanonizace |presenters#Kanonizace]. HTTPS @@ -609,12 +628,11 @@ class MyRouter implements Nette\Routing\Router } ``` -Metoda `match` zpracuje aktuální požadavek [$httpRequest |http:request], ze kterého lze získat nejen URL, ale i hlavičky atd., do pole obsahující název presenteru a jeho parametry. Pokud požadavek zpracovat neumí, vrátí null. -Při zpracování požadavku musíme vrátit minimálně presenter a akci. Název presenteru je úplný a obsahuje i případné moduly: +Metoda `match` zpracuje aktuální požadavek [$httpRequest |http:request], ze kterého lze získat nejen URL, ale i hlavičky atd., do pole obsahující název presenteru a jeho parametry. Pokud požadavek zpracovat neumí, vrátí null. Při zpracování požadavku musíme vrátit minimálně presenter a akci. Název presenteru je úplný a obsahuje i případné moduly: ```php [ - 'presenter' => 'Front:Homepage', + 'presenter' => 'Front:Home', 'action' => 'default', ] ``` @@ -643,7 +661,7 @@ Samostatným použitím myslíme využití schopností routeru v aplikaci, kter Takže opět si vytvoříme metodu, která nám sestaví router, např.: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -674,7 +692,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Anebo objekty přímo vyrobíme: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/cs/templates.texy b/application/cs/templates.texy index 52718200ea..907df592c3 100644 --- a/application/cs/templates.texy +++ b/application/cs/templates.texy @@ -37,27 +37,75 @@ Ta definuje blok `content`, který se vloží na místo `{include content}` v la Hledání šablon -------------- -Cestu k šablonám odvodí presenter podle jednoduché logiky. Zkusí, zda existuje jeden z těchto souborů umístěných relativně od adresáře s třídou presenteru, kde `` je název aktuálního presenteru a `` je název aktuální akce: +Nemusíte v presenterech uvádět, jaká šablona se má vykreslit, framework cestu odvodí sám a ušetří vám psaní. -- `templates//.latte` -- `templates/..latte` +Pokud používáte adresářovou strukturu, kde každý presenter má vlastní adresář, jednodušše umístěte šablonu do tohoto adresáře pod jménem akce (resp. view), tj. pro akci `default` použijte šablonu `default.latte`: -Pokud šablonu nenajde, je odpovědí [chyba 404|presenters#Chyba 404 a spol.]. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -Můžete také změnit view pomocí `$this->setView('jineView')`. Nebo místo dohledávání přímo určit jméno souboru se šablonou pomocí `$this->template->setFile('/path/to/template.latte')`. +Pokud používáte strukturu, kde jsou společně presentery v jednom adresáři a šablony ve složce `templates`, uložte ji buď do souboru `..latte` nebo `/.latte`: + +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1. varianta + └── Home/ + └── default.latte ← 2. varianta +\-- + +Adresář `templates` může být umístěn také o úroveň výš, tj. na stejné úrovni, jako je adresář s třídami presenterů. + +Pokud se šablona nenajde, presenter odpoví [chybou 404 - page not found |presenters#Chyba 404 a spol]. + +View změníte pomocí `$this->setView('jineView')`. Také lze přímo určit soubor se šablonou pomocí `$this->template->setFile('/path/to/template.latte')`. .[note] Soubory, kde se dohledávají šablony, lze změnit překrytím metody [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], která vrací pole možných názvů souborů. -Layout se očekává v těchto souborech: -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` layout společný pro více presenterů +Hledání šablony layoutu +----------------------- + +Nette také automaticky dohledává soubor s layoutem. + +Pokud používáte adresářovou strukturu, kde každý presenter má vlastní adresář, umístěte layout buď do složky s presenterem, pokud je specifický jen pro něj, nebo o úroveň výš, pokud je společný pro více presenterů: -Kde `` je název aktuálního presenteru a `` je název layoutu, což je standardně `'layout'`. Název lze změnit pomocí `$this->setLayout('jinyLayout')`, takže se budou zkoušet soubory `@jinyLayout.latte`. +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← společný layout + └── Home/ + ├── @layout.latte ← jen pro presenter Home + ├── HomePresenter.php + └── default.latte +\-- -Můžete také přímo určit jméno souboru se šablonou layoutu pomocí `$this->setLayout('/path/to/template.latte')`. Pomocí `$this->setLayout(false)` se dohledávání layoutu vypne. +Pokud používáte strukturu, kde jsou společně presentery v jednom adresáři a šablony ve složce `templates`, bude se layout očekávat na těchto místech: + +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← společný layout + ├── Home.@layout.latte ← jen pro Home, 1. varianta + └── Home/ + └── @layout.latte ← jen pro Home, 2. varianta +\-- + +Pokud se presenter nachází v modulu, bude se dohledávat i o další adresářové úrovně výš, podle zanoření modulu. + +Název layoutu lze změnit pomocí `$this->setLayout('layoutAdmin')` a pak se bude očekávat v souboru `@layoutAdmin.latte`. Také lze přímo určit soubor se šablonou layoutu pomocí `$this->setLayout('/path/to/template.latte')`. + +Pomocí `$this->setLayout(false)` nebo značky `{layout none}` uvnitř šablony se dohledávání layoutu vypne. .[note] Soubory, kde se dohledávají šablony layoutu, lze změnit překrytím metody [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], která vrací pole možných názvů souborů. @@ -86,11 +134,8 @@ class ArticlePresenter extends Nette\Application\UI\Presenter class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template { - /** @var Model\Article */ - public $article; - - /** @var Nette\Security\User */ - public $user; + public Model\Article $article; + public Nette\Security\User $user; // a další proměnné } @@ -102,10 +147,10 @@ Anotace `@property-read` je určená pro IDE a statickou analýzu, díky ní bud [* phpstorm-completion.webp *] -Luxusu našeptávání si můžete dopřát i v šablonách, stačí do PhpStorm nainstalovat plugin pro Latte a uvést na začátek šablony název třídy, více v článku "Latte: jak na typový systém":https://blog.nette.org/cs/latte-jak-na-typovy-system: +Luxusu našeptávání si můžete dopřát i v šablonách, stačí do PhpStorm nainstalovat plugin pro Latte a uvést na začátek šablony název třídy, více v článku "Latte: jak na typový systém":https://blog.nette.org/en/latte-how-to-use-type-system: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -134,7 +179,7 @@ Presentery a komponenty předávají do šablon několik užitečných proměnn - `$user` je objekt [reprezentující uživatele |security:authentication] - `$presenter` je aktuální presenter - `$control` je aktuální komponenta nebo presenter -- `$flashes` pole [zpráv |presenters#flash zprávy] zaslaných funkcí `flashMessage()` +- `$flashes` pole [zpráv |presenters#Flash zprávy] zaslaných funkcí `flashMessage()` Pokud používáte vlastní třídu šablony, tyto proměnné se předají, pokud pro ně vytvoříte property. @@ -151,7 +196,7 @@ V šabloně se vytvářejí odkazy na další presentery & akce tímto způsobem Atribut `n:href` je velmi šikovný pro HTML značky ``. Chceme-li odkaz vypsat jinde, například v textu, použijeme `{link}`: ```latte -Adresa je: {link Homepage:default} +Adresa je: {link Home:default} ``` Více informací najdete v kapitole [Vytváření odkazů URL|creating-links]. @@ -174,10 +219,10 @@ public function beforeRender(): void } ``` -Latte ve verzi 3 nabízí pokročilejší způsob a to vytvoření si [extension |latte:creating-extension] pro každý webový projekt. Kusý příklad takové třídy: +Latte ve verzi 3 nabízí pokročilejší způsob a to vytvoření si [extension |latte:extending-latte#Latte Extension] pro každý webový projekt. Kusý příklad takové třídy: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -215,5 +260,64 @@ Zaregistrujeme ji pomocí [konfigurace |configuration#Šablony Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension +``` + + +Překládání +---------- + +Pokud programujete vícejazyčnou aplikaci, budete nejspíš potřebovat některé texty v šabloně vypsat v různých jazycích. Nette Framework k tomuto účelu definuje rozhraní pro překlad [api:Nette\Localization\Translator], které má jedinou metodu `translate()`. Ta přijímá zprávu `$message`, což zpravidla bývá řetězec, a libovolné další parametry. Úkolem je vrátit přeložený řetězec. V Nette není žádná výchozí implementace, můžete si vybrat podle svých potřeb z několika hotových řešeních, které najdete na [Componette |https://componette.org/search/localization]. V jejich dokumentaci se dozvíte, jak translator konfigurovat. + +Šablonám lze nastavit překladač, který si [necháme předat |dependency-injection:passing-dependencies], metodou `setTranslator()`: + +```php +protected function beforeRender(): void +{ + // ... + $this->template->setTranslator($translator); +} ``` + +Translator je alternativně možné nastavit pomocí [konfigurace |configuration#Šablony Latte]: + +```neon +latte: + extensions: + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) +``` + +Poté lze překladač používat například jako filtr `|translate`, a to včetně doplňujících parametrů, které se předají metodě `translate()` (viz `foo, bar`): + +```latte +{='Košík'|translate} +{$item|translate} +{$item|translate, foo, bar} +``` + +Nebo jako podtržítkovou značku: + +```latte +{_'Košík'} +{_$item} +{_$item, foo, bar} +``` + +Pro překlad úseku šablony existuje párová značka `{translate}` (od Latte 2.11, dříve se používala značka `{_}`): + +```latte +{translate}Objednávka{/translate} +{translate foo, bar}Objednávka{/translate} +``` + +Translator se standardně volá za běhu při vykreslování šablony. Latte verze 3 ovšem umí všechny statické texty překládat už během kompilace šablony. Tím se ušetří výkon, protože každý řetězec se přeloží jen jednou a výsledný překlad se zapíše do zkompilované podoby. V adresáři s cache tak vznikne více zkompilovaných verzí šablony, jedna pro každý jazyk. K tomu stačí pouze uvést jazyk jako druhý parametr: + +```php +protected function beforeRender(): void +{ + // ... + $this->template->setTranslator($translator, $lang); +} +``` + +Statickým textem je myšleno třeba `{_'hello'}` nebo `{translate}hello{/translate}`. Nestatické texty, jako třeba `{_$foo}`, se nadále budou překládat za běhu. diff --git a/application/en/@home.texy b/application/en/@home.texy index fac8c0d767..3999c48492 100644 --- a/application/en/@home.texy +++ b/application/en/@home.texy @@ -2,35 +2,84 @@ Nette Application ***************** .[perex] -The `nette/application` package is the basis for creating interactive web applications. - -- [How do applications work? |how-it-works] -- [Bootstrap] -- [Presenters] -- [Templates] -- [Modules] -- [Routing] -- [Creating URL Links |creating-links] -- [Interactive Components |components] -- [AJAX & Snippets |ajax] -- [Multiplier |multiplier] -- [Configuration] +Nette Application is the core of the Nette framework, providing powerful tools for creating modern web applications. It offers a range of exceptional features that significantly simplify development and improve code security and maintainability. Installation ------------ -Download and install the package using [Composer|best-practices:composer]: +Download and install the library using [Composer|best-practices:composer]: ```shell composer require nette/application ``` -| version | compatible with PHP -|-----------|------------------- -| Nette Application 4.0 | PHP 8.0 – 8.2 -| Nette Application 3.1 | PHP 7.2 – 8.2 + +Why choose Nette Application? +----------------------------- + +Nette has always been a pioneer in web technologies. + +**Bidirectional Router:** Nette features an advanced routing system unique in its bidirectionality - it not only translates URLs to application actions but can also generate URLs in reverse. This means: +- You can modify the URL structure of the entire application at any time without needing to modify templates +- URLs are automatically canonicalized, improving SEO +- Routing is defined in one place, not scattered in annotations + +**Components and Signals:** The built-in component system inspired by Delphi and React.js is unique among PHP frameworks: +- Enables creating reusable UI elements +- Supports hierarchical component composition +- Offers elegant AJAX request handling using signals +- Rich library of ready-made components on [Componette](https://componette.org) + +**AJAX and Snippets:** Nette introduced a revolutionary way of working with AJAX in 2009, long before similar solutions like Hotwire for Ruby on Rails or Symfony UX Turbo: +- Snippets allow updating only parts of the page without needing to write JavaScript +- Automatic integration with the component system +- Smart invalidation of page sections +- Minimal data transfer + +**Intuitive [Latte|latte:] Templates:** The most secure templating system for PHP with advanced features: +- Automatic XSS protection with context-sensitive escaping +- Extensible with custom filters, functions, and tags +- Template inheritance and snippets for AJAX +- Excellent PHP 8.x support with type system + +**Dependency Injection:** Nette fully utilizes Dependency Injection: +- Automatic dependency passing (autowiring) +- Configuration using clear NEON format +- Support for component factories + + +Main Benefits +------------- + +- **Security**: Automatic protection against [vulnerabilities|nette:vulnerability-protection] like XSS, CSRF, etc. +- **Productivity**: Less writing, more features thanks to smart design +- **Debugging**: [Tracy debugger|tracy:] with routing panel +- **Performance**: Smart cache, lazy loading of components +- **Flexibility**: Easy URL modification even after application completion +- **Components**: Unique system of reusable UI elements +- **Modern**: Full support for PHP 8.4+ and type system + + +Getting Started +--------------- + +1. [How Applications Work? |how-it-works] - Understanding the basic architecture +2. [Presenters |presenters] - Working with presenters and actions +3. [Templates |templates] - Creating templates in Latte +4. [Routing |routing] - Configuring URL addresses +5. [Interactive Components |components] - Using the component system + + +PHP Compatibility +----------------- + +| version | compatible with PHP +|-----------------------|------------------- +| Nette Application 4.0 | PHP 8.1 – 8.4 +| Nette Application 3.2 | PHP 8.1 – 8.4 +| Nette Application 3.1 | PHP 7.2 – 8.3 | Nette Application 3.0 | PHP 7.1 – 8.0 | Nette Application 2.4 | PHP 5.6 – 8.0 -Applies to the latest patch versions. +Valid for the latest patch versions. diff --git a/application/en/@left-menu.texy b/application/en/@left-menu.texy index 3506cff2c9..0b0defb537 100644 --- a/application/en/@left-menu.texy +++ b/application/en/@left-menu.texy @@ -1,10 +1,10 @@ Nette Application ***************** -- [How do applications work? |how-it-works] -- [Bootstrap] +- [How Do Applications Work? |how-it-works] +- [Bootstrapping] - [Presenters] - [Templates] -- [Modules] +- [Directory Structure |directory-structure] - [Routing] - [Creating URL Links |creating-links] - [Interactive Components |components] @@ -15,5 +15,8 @@ Nette Application Further Reading *************** +- [Why Use Nette?|www:10-reasons-why-nette] +- [Installation |nette:installation] +- [Create Your First Application! |quickstart:] - [Best practices |best-practices:] - [Troubleshooting |nette:troubleshooting] diff --git a/application/en/@meta.texy b/application/en/@meta.texy new file mode 100644 index 0000000000..42471908b0 --- /dev/null +++ b/application/en/@meta.texy @@ -0,0 +1 @@ +{{sitename: Nette Documentation}} diff --git a/application/en/ajax.texy b/application/en/ajax.texy index 5923ffda52..c917e39b91 100644 --- a/application/en/ajax.texy +++ b/application/en/ajax.texy @@ -3,36 +3,43 @@ AJAX & Snippets
    -Modern web applications nowadays run half on a server and half in a browser. AJAX is a vital uniting factor. What support does the Nette Framework offer? -- sending template fragments (so-called *snippets*) +In the era of modern web applications, where functionality is often distributed between the server and the browser, AJAX is an essential connecting element. What options does the Nette Framework offer in this area? +- sending parts of the template, known as snippets - passing variables between PHP and JavaScript -- AJAX applications debugging +- tools for debugging AJAX requests
    -An AJAX request can be detected using a method of a service [encapsulating a HTTP request |http:request] `$httpRequest->isAjax()` (detects based on the `X-Requested-With` HTTP header). There is also a shorthand method in presenter: `$this->isAjax()`. -An AJAX request is no different from a normal one – a presenter is called with a certain view and parameters. It is, too, up to the presenter how will it react: it can use its routines to either return a fragment of HTML code (a snippet), an XML document, a JSON object or a piece of Javascript code. +AJAX Request +============ -There is a pre-processed object called `payload` dedicated to sending data to the browser in JSON. +An AJAX request does not fundamentally differ from a classic HTTP request. A presenter is called with specific parameters. It is up to the presenter to decide how to respond to the request - it can return data in JSON format, send a part of HTML code, an XML document, etc. -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +On the browser side, we initiate an AJAX request using the `fetch()` function: + +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // process the response +}); ``` -For a full control over your JSON output use the `sendJson` method in your presenter. It terminates presenter immediately and you'll do without a template: +On the server side, an AJAX request is recognized by the `$httpRequest->isAjax()` method of the service [encapsulating the HTTP request |http:request]. It uses the `X-Requested-With` HTTP header for detection, so it is crucial to send it. Within the presenter, you can use the `$this->isAjax()` method. + +If you want to send data in JSON format, use the [`sendJson()` |presenters#Sending a Response] method. The method also terminates the presenter's activity. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -If we want to send HTML, we can either set a special template for AJAX requests: +If you plan to respond with a special template designed for AJAX, you can do it as follows: ```php public function handleClick($param): void @@ -45,10 +52,20 @@ public function handleClick($param): void ``` +Snippets +======== + +The most powerful tool offered by Nette for connecting the server with the client are snippets. With them, you can turn an ordinary application into an AJAX one with minimal effort and just a few lines of code. The Fifteen example demonstrates how it all works, and its code can be found on [GitHub |https://github.com/nette-examples/fifteen]. + +Snippets allow you to update only parts of the page, instead of reloading the entire page. This is not only faster and more efficient but also provides a more comfortable user experience. Snippets might remind you of Hotwire for Ruby on Rails or Symfony UX Turbo. Interestingly, Nette introduced snippets 14 years earlier. + +How do snippets work? When the page is first loaded (a non-AJAX request), the entire page, including all snippets, is loaded. When the user interacts with the page (e.g., clicks a button, submits a form, etc.), an AJAX request is initiated instead of reloading the entire page. The code in the presenter performs the action and decides which snippets need updating. Nette renders these snippets and sends them as a JSON payload containing an array with snippets. The handling code in the browser then inserts the received snippets back into the page. Thus, only the code of the changed snippets is transferred, saving bandwidth and speeding up loading compared to transferring the entire page content. + + Naja -==== +---- -The [Naja library|https://naja.js.org] is used to handle AJAX requests on the browser side. [Install |https://naja.js.org/#/guide/01-install-setup-naja] it as a node.js package (to use with Webpack, Rollup, Vite, Parcel and more): +To handle snippets on the browser side, the [Naja library |https://naja.js.org] is used. [Install it |https://naja.js.org/#/guide/01-install-setup-naja] as a Node.js package (for use with applications like Webpack, Rollup, Vite, Parcel, and others): ```shell npm install naja @@ -60,59 +77,55 @@ npm install naja ``` +First, you need to [initialize |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] the library: -Snippets -======== +```js +naja.initialize(); +``` + +To turn an ordinary link (signal) or form submission into an AJAX request, simply mark the relevant link, form, or button with the `ajax` class: + +```html +Go -There is a far more powerful tool of built-in AJAX support – snippets. Using them makes it possible to turn a regular application into an AJAX one using only a few lines of code. How it all works is demonstrated in the Fifteen example whose code is also accessible in the build or on [GitHub |https://github.com/nette-examples/fifteen]. +
    + +
    -The way snippets work is that the whole page is transferred during the initial (i.e. non-AJAX) request and then with every AJAX [subrequest |components#signal] (request of the same view of the same presenter) only the code of the changed parts is transferred in the `payload` repository mentioned earlier. +or -Snippets may remind you of Hotwire for Ruby on Rails or Symfony UX Turbo, but Nette came up with them fourteen years earlier. +
    + +
    +``` -Invalidation of Snippets -======================== +Redrawing Snippets +------------------ -Each descendant of class [Control |components] (which a Presenter is too) is able to remember whether there were any changes during a request that require it to re-render. There are a pair of methods to handle this: `redrawControl()` and `isControlInvalid()`. An example: +Every object of the [Control |components] class (including the Presenter itself) keeps track of whether changes have occurred that require it to be redrawn. The `redrawControl()` method is used for this purpose: ```php public function handleLogin(string $user): void { - // The object has to re-render after the user has logged in + // after login, it is necessary to redraw the relevant part $this->redrawControl(); // ... } ``` -Nette however offers an even finer resolution than whole components. The listed methods accept the name of a so-called "snippet" as an optional parameter. A "snippet" is basically an element in your template marked for that purpose by a Latte tag, more on that later. Thus it is possible to ask a component to redraw only *parts* of its template. If the entire component is invalidated then all of its snippets are re-rendered. A component is “invalid” also if any of its subcomponents is invalid. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // invalidates the snippet named 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, at least one snippet is invalid +Nette allows for even finer control over what needs to be redrawn. The method can accept the name of the snippet as an argument. Thus, it is possible to invalidate (meaning: force redrawing) at the level of template parts. If the entire component is invalidated, every snippet within it will also be redrawn: -$this->redrawControl(); // invalidates the whole component, every snippet -$this->isControlInvalid('footer'); // -> true +```php +// invalidates the 'header' snippet +$this->redrawControl('header'); ``` -A component which receives a signal is automatically marked for redrawing. - -Thanks to snippet redrawing we know exactly which parts of which elements should be re-rendered. - - -Tag `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================ - -Rendering of the page proceeds very similarly to a regular request: the same templates are loaded, etc. The vital part is, however, to leave out the parts that are not supposed to reach the output; the other parts shall be associated with an identifier and sent to the user in a comprehensible format for a JavaScript handler. - -Syntax ------- +Snippets in Latte +----------------- -If there is a control or a snippet in the template, we have to wrap it using the `{snippet} ... {/snippet}` pair tag - it will make sure that the rendered snippet will be "cut out" and sent to the browser. It will also enclose it in a helper `
    ` tag (it is possible to use a different one). In the following example a snippet named `header` is defined. It may as well represent the template of a component: +Using snippets in Latte is extremely easy. To define a part of the template as a snippet, simply wrap it with the `{snippet}` and `{/snippet}` tags: ```latte {snippet header} @@ -120,7 +133,9 @@ If there is a control or a snippet in the template, we have to wrap it using the {/snippet} ``` -A snippet of a type other than `
    ` or a snippet with additional HTML attributes is achieved by using the attribute variant: +The snippet creates a `
    ` element in the HTML page with a special generated `id`. When the snippet is redrawn, the content of this element is updated. Therefore, it is necessary that when the page is initially rendered, all snippets are also rendered, even if they might be empty at the beginning. + +You can also create a snippet using an element other than `
    ` with an n:attribute: ```latte
    @@ -129,138 +144,106 @@ A snippet of a type other than `
    ` or a snippet with additional HTML attribu ``` -Dynamic Snippets -================ +Snippet Areas +------------- -In Nette you can also define snippets with a dynamic name based on a runtime parameter. This is most suitable for various lists where we need to change just one row but we don't want transfer the whole list along with it. An example of this would be: +Snippet names can also be expressions: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -There is one static snippet called `itemsContainer`, containing several dynamic snippets: `item-0`, `item-1` and so on. +This creates several snippets like `item-0`, `item-1`, etc. If we were to directly invalidate a dynamic snippet (e.g., `item-1`), nothing would be redrawn. The reason is that snippets truly function as excerpts, and only they themselves are rendered directly. However, in the template, there is technically no snippet named `item-1`. It only comes into existence when the code surrounding the snippet, i.e., the foreach loop, is executed. Therefore, we mark the part of the template that needs to be executed using the `{snippetArea}` tag: -You can't redraw a dynamic snippet directly (redrawing of `item-1` has no effect), you have to redraw its parent snippet (in this example `itemsContainer`). This causes the code of the parent snippet to be executed, but then just its sub-snippets are sent to the browser. If you want to send over just one of the sub-snippets, you have to modify input for the parent snippet to not generate the other sub-snippets. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -In the example above you have to make sure that for an AJAX request only one item will be added to the `$list` array, therefore the `foreach` loop will print just one dynamic snippet. +And we request the redrawing of both the individual snippet and the entire parent area: ```php -class HomepagePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third' - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +At the same time, it is advisable to ensure that the `$items` array contains only the items that should be redrawn. -Snippets in an Included Template -================================ - -It can happen that the snippet is in a template which is being included from a different template. In that case we need to wrap the inclusion code in the second template with the `snippetArea` tag, then we redraw both the snippetArea and the actual snippet. - -Tag `snippetArea` ensures that the code inside is executed but only the actual snippet in the included template is sent to the browser. +If we include another template containing snippets into the main template using the `{include}` tag, it is necessary to wrap the template inclusion within a `snippetArea` again and invalidate it along with the snippet: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -You can also combine it with dynamic snippets. - -Adding and Deleting -=================== +Snippets in Components +---------------------- -If you add a new item into the list and invalidate `itemsContainer`, the AJAX request returns snippets including the new one, but the javascript handler won’t be able to render it. This is because there is no HTML element with the newly created ID. - -In this case, the simplest way is to wrap the whole list in one more snippet and invalidate it all: +You can create snippets within [components|components], and Nette will automatically redraw them. However, there is a limitation: to redraw snippets, Nette calls the `render()` method without any parameters. Therefore, passing parameters in the template will not work: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Sending Custom Data +------------------- + +Along with snippets, you can send any additional data to the client. Simply write them into the `payload` object: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + // ... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -The same goes for deleting an item. It would be possible to send empty snippet, but usually lists can be paginated and it would be complicated to implement deleting one item and loading another (which used to be on a different page of the paginated list). - -Sending Parameters to Component -=============================== +Passing Parameters +================== -When we send parameters to the component via AJAX request, whether signal parameters or persistent parameters, we must provide their global name, which also contains the name of the component. The full name of parameter returns the `getParameterId()` method. +When sending parameters to a component via an AJAX request, whether they are signal parameters or persistent parameters, we must specify their global name in the request, which includes the component's name. The `getParameterId()` method returns the full parameter name. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -And handle method with s corresponding parameters in component. +And the handle method with corresponding parameters in the component: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/en/bootstrap.texy b/application/en/bootstrap.texy deleted file mode 100644 index c7927f9733..0000000000 --- a/application/en/bootstrap.texy +++ /dev/null @@ -1,233 +0,0 @@ -Bootstrap -********* - -
    - -Bootstrap is boot code that initializes the environment, creates a dependency injection (DI) container, and starts the application. We will discuss: - -- how to configure your application using NEON files -- how to handle production and development modes -- how to create the DI container - -
    - - -Applications, whether web-based or command-line scripts, begin by some form of environment initialization. In ancient times, it could be a file named eg `include.inc.php` that was in charge of this, and was included in the initial file. -In modern Nette applications, it has been replaced by the `Bootstrap` class, which as part of the application can be found in the `app/Bootstrap.php`. It might look for example like this: - -```php -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - public static function boot(): Configurator - { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; - } -} -``` - - -index.php -========= - -In the case of web applications, the initial file is `index.php`, which is located in the public directory `www/`. It lets the `Bootstrap` class to initialize the environment and return the `$configurator` which creates DI container. Then it obtains the `Application` service, that runs the web application: - -```php -// initialize the environment + get Configurator object -$configurator = App\Bootstrap::boot(); -// create a DI container -$container = $configurator->createContainer(); -// DI container creates a Nette\Application\Application object -$application = $container->getByType(Nette\Application\Application::class); -// start Nette application -$application->run(); -``` - -As you can see, the [api:Nette\Bootstrap\Configurator] class, which we will now introduce in more detail, helps with setting up the environment and creating a dependency injection (DI) container. - - -Development vs Production Mode -============================== - -Nette distinguishes between two basic modes in which a request is executed: development and production. The development mode is focused on the maximum comfort of the programmer, Tracy is displayed, the cache is automatically updated when changing templates or DI container configuration, etc. Production mode is focused on performance, Tracy only logs errors and changes of templates and other files are not checked. - -Mode selection is done by autodetection, so there is usually no need to configure or switch anything manually. The mode is development if the application is running on localhost (ie IP address `127.0.0.1` or `::1`) and no proxy is present (ie its HTTP header). Otherwise, it runs in production mode. - -If you want to enable development mode in other cases, for example, for programmers accessing from a specific IP address, you can use `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); // one or more IP addresses -``` - -We definitely recommend combining an IP address with a cookie. We will store a secret token into the `nette-debug` cookie, e.g. `secret1234`, and the development mode will be activated for programmers with this combination of IP and cookie. - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -We can also turn off developer mode completely, even for localhost: - -```php -$configurator->setDebugMode(false); -``` - -Note that the value `true` turns on developer mode by hard, which should never happen on a production server. - - -Debugging Tool Tracy -==================== - -For easy debugging, we will turn on the great tool [Tracy |tracy:]. In developer mode it visualizes errors and in production mode it logs errors to the specified directory: - -```php -$configurator->enableTracy($appDir . '/log'); -``` - - -Temporary Files -=============== - -Nette uses the cache for DI container, RobotLoader, templates, etc. Therefore it is necessary to set the path to the directory where the cache will be stored: - -```php -$configurator->setTempDirectory($appDir . '/temp'); -``` - -On Linux or macOS, set the [write permissions |nette:troubleshooting#Setting directory permissions] for directories `log/` and `temp/`. - - -RobotLoader -=========== - -Usually, we will want to automatically load the classes using [RobotLoader |robot-loader:], so we have to start it up and let it load classes from the directory where `Bootstrap.php` is located (i.e. `__DIR__`) and all its subdirectories: - -```php -$configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -An alternative way is to only use [Composer |best-practices:composer] PSR-4 autoloading. - - -Timezone -======== - -Configurator allows you to specify a timezone for your application. - -```php -$configurator->setTimeZone('Europe/Prague'); -``` - - -DI Container Configuration -========================== - -Part of the boot process is the creation of a DI container, ie a factory for objects, which is the heart of the whole application. It is actually a PHP class generated by Nette and stored in a cache directory. The factory produces key application objects and configuration files instruct it how to create and configure them, and thus we influence the behavior of the whole application. - -Configuration files are usually written in the [NEON format|neon:format]. You can read [what can be configured here|nette:configuring]. - -.[tip] -In the development mode, the container is automatically updated each time you change the code or configuration files. In production mode, it is generated only once and file changes are not checked to maximize performance. - -Configuration files are loaded using `addConfig()`: - -```php -$configurator->addConfig($appDir . '/config/common.neon'); -``` - -The method `addConfig()` can be called multiple times to add multiple files. - -```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); -if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); -} -``` - -The name `cli.php` is not a typo, the configuration can also be written in a PHP file, which returns it as an array. - -Alternatively, we can use the [`includes` section|dependency-injection:configuration#including files] to load more configuration files. - -If items with the same keys appear within configuration files, they will be [overwritten or merged |dependency-injection:configuration#Merging] in the case of arrays. Later included file has a higher priority than the previous one. The file in which the `includes` section is listed has a higher priority than the files included in it. - - -Static Parameters ------------------ - -Parameters used in configuration files can be defined [in the section `parameters`|dependency-injection:configuration#parameters] and also passed (or overwritten) by the `addStaticParameters()` method (it has alias `addParameters()`). It is important that different parameter values cause the generation of additional DI containers, i.e. additional classes. - -```php -$configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -In configuration files, we can write usual notation `%projectId%` to access the parameter named `projectId`. By default, the Configurator populates the following parameters: `appDir`, `wwwDir`, `tempDir`, `vendorDir`, `debugMode` and `consoleMode`. - - -Dynamic Parameters ------------------- - -We can also add dynamic parameters to the container, their different values, unlike static parameters, will not cause the generation of new DI containers. - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Environment variables could be easily made available using dynamic parameters. We can access them via `%env.variable%` in configuration files. - -```php -$configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Imported Services ------------------ - -We're going deeper now. Although the purpose of a DI container is to create objects, exceptionally there may be a need to insert an existing object into the container. We do this by defining the service with the `imported: true` attribute. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -Create a new instance and insert it in bootstrap: - -```php -$configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Different Environments -====================== - -Feel free to customize the `Bootstrap` class to suit your needs. You can add parameters to the `boot()` method to differentiate web projects, or add other methods, such as `bootForTests()`, which initializes the environment for unit tests, `bootForCli()` for scripts called from the command line, and so on. - -```php -public static function bootForTests(): Configurator -{ - $configurator = self::boot(); - Tester\Environment::setup(); // Nette Tester initialization - return $configurator; -} -``` diff --git a/application/en/bootstrapping.texy b/application/en/bootstrapping.texy new file mode 100644 index 0000000000..8ee9c78236 --- /dev/null +++ b/application/en/bootstrapping.texy @@ -0,0 +1,298 @@ +Bootstrapping +************* + +
    + +Bootstrapping is the process of initializing the application environment, creating a dependency injection (DI) container, and starting the application. We will discuss: + +- how the Bootstrap class initializes the environment +- how applications are configured using NEON files +- how to distinguish between production and development mode +- how to create and configure the DI container + +
    + + +Applications, whether web-based or scripts run from the command line, begin their execution with some form of environment initialization. In the old days, a file named perhaps `include.inc.php` was responsible for this, included by the initial file. In modern Nette applications, it has been replaced by the `Bootstrap` class, which, as part of the application, can be found in the `app/Bootstrap.php` file. It might look like this, for example: + +```php +use Nette\Bootstrap\Configurator; + +class Bootstrap +{ + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // The configurator is responsible for setting up the application environment and services. + $this->configurator = new Configurator; + // Set the directory for temporary files generated by Nette (e.g., compiled templates) + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void + { + // Nette is smart, and the development mode turns on automatically, + // or you can enable it for a specific IP address by uncommenting the following line: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Enables Tracy: the ultimate "swiss army knife" debugging tool. + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: automatically loads all classes in the chosen directory + $this->configurator->createRobotLoader() + ->addDirectory(__DIR__) + ->register(); + } + + private function setupContainer(): void + { + // Load configuration files + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); + } +} +``` + + +index.php +========= + +In the case of web applications, the initial file is `index.php`, located in the [public directory |directory-structure#Public Directory www] `www/`. It instructs the Bootstrap class to initialize the environment and create the DI container. Then, it retrieves the `Application` service from the container, which runs the web application: + +```php +$bootstrap = new App\Bootstrap; +// Initialize the environment + create a DI container +$container = $bootstrap->bootWebApplication(); +// DI container creates a Nette\Application\Application object +$application = $container->getByType(Nette\Application\Application::class); +// Start the Nette application and process the incoming request +$application->run(); +``` + +As you can see, the [api:Nette\Bootstrap\Configurator] class helps with setting up the environment and creating the dependency injection (DI) container. We will now introduce it in more detail. + + +Development vs Production Mode +============================== + +Nette behaves differently depending on whether it is running on a development or production server: + +🛠️ Development Mode: + - Displays the Tracy debug bar with useful information (SQL queries, execution time, memory used) + - In case of an error, displays a detailed error page with function calls and variable contents + - Automatically refreshes the cache when Latte templates, configuration files, etc., are changed + + +🚀 Production Mode: + - Does not display any debugging information; all errors are written to the log + - In case of an error, displays an ErrorPresenter or a generic "Server Error" page + - Cache is never automatically refreshed! + - Optimized for speed and security + + +Mode selection is done by autodetection, so usually, there is no need to configure anything or manually switch modes: + +- development mode: on localhost (IP address `127.0.0.1` or `::1`) if no proxy is present (i.e., its HTTP header is not detected) +- production mode: everywhere else + +If we want to enable development mode in other cases, for example, for programmers accessing from a specific IP address, we use `setDebugMode()`: + +```php +$this->configurator->setDebugMode('23.75.345.200'); // an array of IP addresses can also be provided +``` + +We strongly recommend combining an IP address with a cookie. Store a secret token, e.g., `secret1234`, in the `nette-debug` cookie, and this way, activate development mode for programmers accessing from a specific IP address who also have the mentioned token in their cookie: + +```php +$this->configurator->setDebugMode('secret1234@23.75.345.200'); +``` + +We can also disable development mode completely, even for localhost: + +```php +$this->configurator->setDebugMode(false); +``` + +Note that the value `true` forces development mode on, which should **never** happen on a production server. + + +Debugging Tool Tracy +==================== + +For easy debugging, we will enable the excellent tool [Tracy |tracy:]. In development mode, it visualizes errors, and in production mode, it logs errors to the specified directory: + +```php +$this->configurator->enableTracy($this->rootDir . '/log'); +``` + + +Temporary Files +=============== + +Nette uses cache for the DI container, RobotLoader, templates, etc. Therefore, it is necessary to set the path to the directory where the cache will be stored: + +```php +$this->configurator->setTempDirectory($this->rootDir . '/temp'); +``` + +On Linux or macOS, set [write permissions |nette:troubleshooting#Setting Directory Permissions] for the `log/` and `temp/` directories. + + +RobotLoader +=========== + +Usually, we will want to automatically load classes using [RobotLoader |robot-loader:], so we need to start it and let it load classes from the directory where `Bootstrap.php` is located (i.e., `__DIR__`), and all its subdirectories: + +```php +$this->configurator->createRobotLoader() + ->addDirectory(__DIR__) + ->register(); +``` + +An alternative approach is to load classes solely through [Composer |best-practices:composer] following PSR-4. + + +Timezone +======== + +You can set the default timezone via the configurator. + +```php +$this->configurator->setTimeZone('Europe/Prague'); +``` + + +DI Container Configuration +========================== + +Part of the booting process is the creation of the DI container, or object factory, which is the heart of the entire application. It is actually a PHP class generated by Nette and stored in the cache directory. The factory produces key application objects, and using configuration files, we instruct it how to create and set them up, thereby influencing the behavior of the entire application. + +Configuration files are usually written in the [NEON format |neon:format]. In a separate chapter, you can read about [what can be configured |nette:configuring]. + +.[tip] +In development mode, the container is automatically updated whenever the code or configuration files change. In production mode, it is generated only once, and changes are not checked to maximize performance. + +Configuration files are loaded using `addConfig()`: + +```php +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); +``` + +If we want to add more configuration files, we can call the `addConfig()` function multiple times. + +```php +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); +if (PHP_SAPI === 'cli') { + $this->configurator->addConfig($configDir . '/cli.php'); +} +``` + +The name `cli.php` is not a typo; configuration can also be written in a PHP file that returns it as an array. + +We can also add other configuration files in [the `includes` section |dependency-injection:configuration#Including Files]. + +If items with the same keys appear in configuration files, they will be overwritten, or in the case of [arrays, merged |dependency-injection:configuration#Merging]. A file included later has higher priority than the previous one. The file in which the `includes` section is listed has higher priority than the files included within it. + + +Static Parameters +----------------- + +Parameters used in configuration files can be defined [in the `parameters` section |dependency-injection:configuration#Parameters] and also passed (or overridden) using the `addStaticParameters()` method (it has an alias `addParameters()`). It is important that different parameter values will cause the generation of additional DI containers, i.e., additional classes. + +```php +$this->configurator->addStaticParameters([ + 'projectId' => 23, +]); +``` + +The `projectId` parameter can be referenced in the configuration using the standard notation `%projectId%`. + + +Dynamic Parameters +------------------ + +We can also add dynamic parameters to the container, whose different values, unlike static parameters, will not cause the generation of new DI containers. + +```php +$this->configurator->addDynamicParameters([ + 'remoteIp' => $_SERVER['REMOTE_ADDR'], +]); +``` + +This way, we can easily add, for example, environment variables, which can then be referenced in the configuration using the notation `%env.variable%`. + +```php +$this->configurator->addDynamicParameters([ + 'env' => getenv(), +]); +``` + + +Default Parameters +------------------ + +You can use these static parameters in the configuration files: + +- `%appDir%` is the absolute path to the directory containing the `Bootstrap.php` file +- `%wwwDir%` is the absolute path to the directory containing the entry file `index.php` +- `%tempDir%` is the absolute path to the directory for temporary files +- `%vendorDir%` is the absolute path to the directory where Composer installs libraries +- `%rootDir%` is the absolute path to the root directory of the project +- `%baseUrl%` is the absolute URL to the root directory +- `%debugMode%` indicates whether the application is in debug mode +- `%consoleMode%` indicates whether the request came through the command line + + +Imported Services +----------------- + +Now we go deeper. Although the purpose of the DI container is to create objects, occasionally there might be a need to insert an existing object into the container. We do this by defining the service with the `imported: true` flag. + +```neon +services: + myservice: + type: App\Model\MyCustomService + imported: true +``` + +And in the bootstrap, we insert the object into the container: + +```php +$this->configurator->addServices([ + 'myservice' => new App\Model\MyCustomService('foobar'), +]); +``` + + +Different Environments +====================== + +Feel free to modify the `Bootstrap` class according to your needs. You can add parameters to the `bootWebApplication()` method to distinguish between web projects. Or we can add other methods, such as `bootTestEnvironment()` which initializes the environment for unit tests, `bootConsoleApplication()` for scripts called from the command line, etc. + +```php +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // Nette Tester initialization + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container +{ + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); +} +``` diff --git a/application/en/components.texy b/application/en/components.texy index ef88a5b0bd..615b03033b 100644 --- a/application/en/components.texy +++ b/application/en/components.texy @@ -3,7 +3,7 @@ Interactive Components
    -Components are separate reusable objects that we place into pages. They can be forms, datagrids, polls, in fact anything that makes sense to use repeatedly. We will show: +Components are separate reusable objects that we embed into pages. They can be forms, datagrids, polls, essentially anything that makes sense to use repeatedly. We will show: - how to use components? - how to write them? @@ -11,19 +11,19 @@ Components are separate reusable objects that we place into pages. They can be f
    -Nette has a built-in component system. Older of you may remember something similar from Delphi or ASP.NET Web Forms. React or Vue.js is built on something remotely similar. However, in the world of PHP frameworks, this is a completely unique feature. +Nette has a built-in component system. Something similar might be familiar to veterans from Delphi or ASP.NET Web Forms; React or Vue.js is built on something remotely similar. However, in the world of PHP frameworks, this is a unique feature. -At the same time, components fundamentally change the approach to application development. You can compose pages from pre-prepared units. Do you need datagrid in administration? You can find it at [Componette |https://componette.org/search/component], a repository of open-source add-ons (not just components) for Nette, and simply paste it into the presenter. +At the same time, components fundamentally influence the approach to application development. You can compose pages from pre-prepared units. Need a datagrid in your administration? Find it on [Componette |https://componette.org/search/component], a repository of open-source add-ons (not just components) for Nette, and simply insert it into the presenter. -You can incorporate any number of components into the presenter. And you can insert other components into some components. This creates a component tree with a presenter as a root. +You can incorporate any number of components into the presenter. And you can embed other components within some components. This creates a component tree, with the presenter as its root. Factory Methods =============== -How are components placed and subsequently used in the presenter? Usually using factory methods. +How are components inserted into the presenter and subsequently used? Usually via factory methods. -The component factory is an elegant way to create components only when they are really needed (lazy / on-demand). The whole magic is in implementation of a method called `createComponent()`, where `` is the name of the component, that will create and return. +A component factory is an elegant way to create components only when they are actually needed (lazy / on demand). The whole magic lies in implementing a method named `createComponent()`, where `` is the name of the component being created, which creates and returns the component. ```php .{file:DefaultPresenter.php} class DefaultPresenter extends Nette\Application\UI\Presenter @@ -37,21 +37,21 @@ class DefaultPresenter extends Nette\Application\UI\Presenter } ``` -Because all components are created in separate methods, the code is cleaner and easier to read. +Because all components are created in separate methods, the code becomes clearer. .[note] -Component names always start with a lowercase letter, although they are capitalized in the method name. +Component names always start with a lowercase letter, even though they are capitalized in the method name. -We never call factories directly, they get called automatically, when we use components for the first time. Thanks to that, a component is created at the right moment, and only if it's really needed. If we wouldn't use the component (for example on some AJAX request, where we return only part of the page, or when parts are cached), it wouldn't even be created and we save performance of the server. +We never call factories directly; they are called automatically the first time we use the component. Thanks to this, the component is created at the right moment and only if it is actually needed. If we don't use the component (e.g., during an AJAX request where only part of the page is transferred, or when caching the template), it won't be created at all, saving server performance. ```php .{file:DefaultPresenter.php} // we access the component and if it was the first time, -// it calls createComponentPoll() to create it +// createComponentPoll() is called which creates it $poll = $this->getComponent('poll'); // alternative syntax: $poll = $this['poll']; ``` -In the template, you can render a component using tag [{control} |#Rendering]. So there is no need of manually passing the components to template. +In the template, it is possible to render a component using the [{control} |#Rendering] tag. Therefore, there is no need to manually pass components to the template. ```latte

    Please Vote

    @@ -63,17 +63,17 @@ In the template, you can render a component using tag [{control} |#Rendering]. S Hollywood Style =============== -Components commonly use one cool technique, which we like to call Hollywood style. Surely you know the cliché that actors hear often at the casting calls: "Don't call us, we'll call you." And that's what this is about. +Components commonly use a fresh technique we like to call the Hollywood style. You surely know the cliché often heard by participants in film auditions: "Don't call us, we'll call you." And that's precisely what it's about. -In Nette, instead of having to constantly ask questions ("was the form submitted?", "was it valid?" or "did anyone press this button?"), you tell the framework "when this happens, call this method" and leave further work on it. If you program in JavaScript, you are familiar with this style of programming. You write functions that are called when a certain event occurs. And the engine passes the appropriate parameters to them. +In Nette, instead of constantly having to ask questions ("was the form submitted?", "was it valid?", or "did the user press this button?"), you tell the framework "when this happens, call this method" and leave further work to it. If you program in JavaScript, you are intimately familiar with this style of programming. You write functions that are called when a certain event occurs. And the language passes the appropriate parameters to them. -This completely changes the way you write applications. The more tasks you can delegate to the framework, the less work you have. And the less you can forget. +This completely changes the perspective on writing applications. The more tasks you can leave to the framework, the less work you have. And the less you might overlook. -How to Write a Component -======================== +Writing a Component +=================== -By component we usually mean descendants of the class [api:Nette\Application\UI\Control]. The presenter [api:Nette\Application\UI\Presenter] itself is also a descendant of the `Control` class. +By the term component, we usually mean a descendant of the [api:Nette\Application\UI\Control] class. (It would be more accurate to use the term "controls", but that has a different meaning in some languages, and "components" has become more established.) The presenter [api:Nette\Application\UI\Presenter] itself is also a descendant of the `Control` class. ```php .{file:PollControl.php} use Nette\Application\UI\Control; @@ -87,19 +87,19 @@ class PollControl extends Control Rendering ========= -We already know that the `{control componentName}` tag is used to draw a component. It actually calls the method `render()` of the component, in which we take care of the rendering. We have, just like in the presenter, a [Latte |latte:] template in the variable `$this->template`, to which we pass the parameters. Unlike use in a presenter, we must specify a template file and let it render: +We already know that the `{control componentName}` tag is used to render a component. It actually calls the `render()` method of the component, in which we take care of the rendering. We have available, just like in the presenter, a [Latte template|templates] in the `$this->template` variable, to which we pass parameters. Unlike in the presenter, we must specify the template file and have it rendered: ```php .{file:PollControl.php} public function render(): void { - // we will put some parameters into the template + // insert some parameters into the template $this->template->param = $value; - // and draw it + // and render it $this->template->render(__DIR__ . '/poll.latte'); } ``` -The tag `{control}` allows to pass parameters to the method `render()`: +The `{control}` tag allows passing parameters to the `render()` method: ```latte {control poll $id, $message} @@ -112,7 +112,7 @@ public function render(int $id, string $message): void } ``` -Sometimes a component can consist of several parts that we want to render separately. For each of them we will create own rendering method, here is for example `renderPaginator()`: +Sometimes a component may consist of several parts that we want to render separately. For each of them, we create our own rendering method, here in the example, `renderPaginator()`: ```php .{file:PollControl.php} public function renderPaginator(): void @@ -121,69 +121,69 @@ public function renderPaginator(): void } ``` -And in the template we then call it using: +And in the template, we then invoke it using: ```latte {control poll:paginator} ``` -For better understanding it's good to know how the tag is compiled to PHP code. +For a better understanding, it's good to know how this tag translates into PHP code. ```latte {control poll} {control poll:paginator 123, 'hello'} ``` -This compiles to: +translates to: ```php $control->getComponent('poll')->render(); $control->getComponent('poll')->renderPaginator(123, 'hello'); ``` -`getComponent()` method returns the `poll` component and then the `render()` or `renderPaginator()` method, respectively, is called on it. +The `getComponent()` method returns the `poll` component, and the `render()` method, or `renderPaginator()` if a different rendering method is specified in the tag after the colon, is called on this component. .[caution] -If anywhere in the parameter part **`=>`** is used, all parameters will be wrapped with an array and passed as the first argument: +Beware, if **`=>`** appears anywhere in the parameters, all parameters will be wrapped in an array and passed as the first argument: ```latte {control poll, id: 123, message: 'hello'} ``` -compiles to: +translates to: ```php $control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); ``` -Rendering of sub-component: +Rendering a sub-component: ```latte {control cartControl-someForm} ``` -compiles to: +translates to: ```php $control->getComponent("cartControl-someForm")->render(); ``` -Components, like presenters, pass several useful variables to templates automatically: +Components, like presenters, automatically pass several useful variables to templates: -- `$basePath` is an absolute URL path to root dir (for example `/CD-collection`) -- `$baseUrl` is an absolute URL to root dir (for example `http://localhost/CD-collection`) +- `$basePath` is the absolute URL path to the root directory (e.g., `/eshop`) +- `$baseUrl` is the absolute URL to the root directory (e.g., `http://localhost/eshop`) - `$user` is an object [representing the user |security:authentication] - `$presenter` is the current presenter - `$control` is the current component -- `$flashes` list of [messages |#flash-messages] sent by method `flashMessage()` +- `$flashes` is an array of [messages |#Flash Messages] sent by the `flashMessage()` function Signal ====== -We already know that navigation in the Nette application consists of linking or redirecting to pairs `Presenter:action`. But what if we just want to perform an action on the **current page**? For example, change the sorting order of the column in the table; delete item; switch light/dark mode; submit the form; vote in the poll; etc. +We already know that navigation in a Nette application consists of linking or redirecting to `Presenter:action` pairs. But what if we just want to perform an action on the **current page**? For example, change the sorting of columns in a table; delete an item; switch light/dark mode; submit a form; vote in a poll; etc. -This type of request is called a signal. And like actions invoke methods `action()` or `render()`, signals call methods `handle()`. While the concept of action (or view) relates only to presenters, signals apply to all components. And therefore also to presenters, because `UI\Presenter` is a descendant of `UI\Control`. +This type of request is called a signal. And just as actions invoke `action()` or `render()` methods, signals call `handle()` methods. While the concept of action (or view) relates purely to presenters, signals concern all components. And thus also presenters, because `UI\Presenter` is a descendant of `UI\Control`. ```php public function handleClick(int $x, int $y): void @@ -192,36 +192,36 @@ public function handleClick(int $x, int $y): void } ``` -The link that calls the signal is created in the usual way, i.e. in the template by the attribute `n:href` or the tag `{link}`, in the code by the method `link()`. More in the chapter [Creating URL links |creating-links#Links to Signal]. +A link that calls a signal is created in the usual way, i.e., in the template with the `n:href` attribute or the `{link}` tag, in the code with the `link()` method. More in the chapter [Creating URL Links |creating-links#Links to Signal]. ```latte click here ``` -The signal is always called on the current presenter and view, so it is not possible to link to signal in different presenter / action. +A signal is always called on the current presenter and action; it is not possible to invoke it on another presenter or action. -Thus, the signal causes the page to be reloaded in exactly the same way as in the original request, only in addition it calls the signal handling method with the appropriate parameters. If the method does not exist, exception [api:Nette\Application\UI\BadSignalException] is thrown, which is displayed to the user as error page 403 Forbidden. +Thus, a signal causes the page to reload just like the original request, but additionally calls the signal handling method with the appropriate parameters. If the method does not exist, an [api:Nette\Application\UI\BadSignalException] exception is thrown, which is displayed to the user as a 403 Forbidden error page. Snippets and AJAX ================= -The signals may remind you a little bit AJAX: handlers that are called on the current page. And you're right, signals are really often called using AJAX, and then we only transmit changed parts of the page to the browser. They are called snippets. More information can be found on [the page about AJAX |ajax]. +Signals might remind you a bit of AJAX: handlers that are invoked on the current page. And you are right, signals are indeed often called using AJAX, and subsequently, only the changed parts of the page are transferred to the browser. These are called snippets. More information can be found on the [page dedicated to AJAX |ajax]. Flash Messages ============== -A component has its own storage of flash messages independent of the presenter. These are messages that, for example, inform about the result of the operation. An important feature of flash messages is that they are available in the template even after redirection. Even after being displayed, they will remain alive for another 30 seconds - for example, in case the user would unintentionally refresh the page - the message will not be lost. +A component has its own storage for flash messages, independent of the presenter. These are messages that, for example, inform about the result of an operation. An important feature of flash messages is that they are available in the template even after redirection. Even after being displayed, they remain active for another 30 seconds – for example, in case the user refreshes the page due to a transmission error - the message won't disappear immediately. -Sending is done by the method [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. The first parameter is the message text or the `stdClass` object representing the message. Optional second parameter is its type (error, warning, info, etc.). The method `flashMessage()` returns an instance of flash message as object stdClass to which you can pass information. +Sending is handled by the [flashMessage |api:Nette\Application\UI\Control::flashMessage()] method. The first parameter is the message text or an `stdClass` object representing the message. The optional second parameter is its type (error, warning, info, etc.). The `flashMessage()` method returns an instance of the flash message as an `stdClass` object, to which further information can be added. ```php $this->flashMessage('Item was deleted.'); $this->redirect(/* ... */); // and redirect ``` -In the template, these messages are available in the variable `$flashes` as objects `stdClass`, which contain the properties `message` (message text), `type` (message type) and can contain the already mentioned user information. We draw them as follows: +These messages are available to the template in the `$flashes` variable as `stdClass` objects, which contain the properties `message` (message text), `type` (message type), and can contain the aforementioned user information. We render them like this, for example: ```latte {foreach $flashes as $flash} @@ -230,41 +230,66 @@ In the template, these messages are available in the variable `$flashes` as obje ``` -Persistent Parameters -===================== +Redirection After a Signal +========================== -It is often needed to keep some parameter in a component for the whole time of working with the component. It can be for example the number of the page in pagination. This parameter should be marked as persistent using the annotation `@persistent`. +Processing a component's signal is often followed by a redirect. This is similar to forms - after submitting them, we also redirect to prevent data resubmission if the page is refreshed in the browser. ```php -class PollControl extends Control -{ - /** @persistent */ - public $page = 1; -} +$this->redirect('this'); // redirects to the current presenter and action ``` -This parameter will be automatically passed in every link as a `GET` parameter until the user leaves the page with this component. +Because a component is a reusable element and typically should not have a direct link to specific presenters, the `redirect()` and `link()` methods automatically interpret the parameter as a component signal: -.[caution] -Never trust persistent parameters blindly because they can be faked easily (by overwriting the URL). Verify, for example, if the page number is within the correct interval. +```php +$this->redirect('click'); // redirects to the 'click' signal of the same component +``` -In PHP 8, you can also use attributes to mark persistent parameters: +If you need to redirect to another presenter or action, you can do it through the presenter: ```php -use Nette\Application\Attributes\Persistent; +$this->getPresenter()->redirect('Product:show'); // redirects to another presenter/action +``` -class PollControl extends Control + +Persistent Parameters +===================== + +Persistent parameters are used to maintain state in components across different requests. Their value remains the same even after clicking a link. Unlike session data, they are transferred in the URL. And this happens completely automatically, including links created in other components on the same page. + +For example, you have a component for content pagination. There might be several such components on a page. And we want all components to remain on their current page after clicking a link. Therefore, we make the page number (`page`) a persistent parameter. + +Creating a persistent parameter in Nette is extremely simple. Just create a public property and mark it with the attribute: (previously `/** @persistent */` was used) + +```php +use Nette\Application\Attributes\Persistent; // this line is important + +class PaginatingControl extends Control { #[Persistent] - public $page = 1; + public int $page = 1; // must be public } ``` +We recommend specifying the data type for the property (e.g., `int`), and you can also provide a default value. Parameter values can be [validated |#Validation of Persistent Parameters]. + +When creating a link, the value of a persistent parameter can be changed: + +```latte +next +``` + +Or it can be *reset*, i.e., removed from the URL. It will then assume its default value: + +```latte +reset +``` + Persistent Components ===================== -Not only parameters but also components can be persistent. Their persistent parameters are also transferred between different actions or between different presenters. We mark persistent components with this annotations for the presenter class. For example here we mark components `calendar` and `poll` as follows: +Not only parameters but also components can be persistent. For such a component, its persistent parameters are transferred even between different actions of the presenter or between multiple presenters. Persistent components are marked with an annotation in the presenter class. For example, we mark the `calendar` and `poll` components like this: ```php /** @@ -275,7 +300,7 @@ class DefaultPresenter extends Nette\Application\UI\Presenter } ``` -You don't have to mark subcomponents as persistent, they are persistent automatically. +Subcomponents within these components do not need to be marked; they become persistent too. In PHP 8, you can also use attributes to mark persistent components: @@ -292,48 +317,39 @@ class DefaultPresenter extends Nette\Application\UI\Presenter Components with Dependencies ============================ -How to create components with dependencies without "messing up" the presenters that will use them? Thanks to the clever features of the DI container in Nette, as with using traditional services, we can leave most of the work to the framework. +How to create components with dependencies without "cluttering" the presenters that will use them? Thanks to the smart features of the DI container in Nette, similar to using classic services, most of the work can be left to the framework. -Let's take as an example a component that has a dependency on the `PollFacade` service: +Let's take an example of a component that has a dependency on the `PollFacade` service: ```php class PollControl extends Control { - /** @var PollFacade */ - private $facade; - - /** @var int Id of a poll, for which the component is created */ - private $id; - - public function __construct(int $id, PollFacade $facade) - { - $this->facade = $facade; - $this->id = $id; + public function __construct( + private int $id, // ID of the poll for which we are creating the component + private PollFacade $facade, + ) { } public function handleVote(int $voteId): void { - $this->facade->vote($id, $voteId); - //... + $this->facade->vote($this->id, $voteId); + // ... } } ``` -If we were writing a classic service, there would be nothing to worry about. The DI container would invisibly take care of passing all the dependencies. But we usually handle components by creating a new instance of them directly in the presenter in [#factory methods] `createComponent...()`. But passing all the dependencies of all the components to the presenter to then pass them to the components is cumbersome. And the amount of code written... +If we were writing a classic service, there would be nothing to discuss. The DI container would invisibly handle passing all dependencies. However, with components, we usually handle them by creating a new instance directly in the presenter within the [#factory methods] `createComponent…()`. But passing all dependencies of all components into the presenter just to pass them on to the components is cumbersome. And the amount of code written... -The logical question is, why don't we just register the component as a classic service, pass it to the presenter, and then return it in the `createComponent...()` method? But this approach is inappropriate because we want to be able to create the component multiple times. +The logical question is, why don't we simply register the component as a classic service, pass it to the presenter, and then return it in the `createComponent…()` method? However, this approach is inappropriate because we want the ability to create the component multiple times if needed. -The correct solution is to write a factory for the component, i.e. a class that creates the component for us: +The correct solution is to write a factory for the component, i.e., a class that creates the component for us: ```php class PollControlFactory { - /** @var PollFacade */ - private $facade; - - public function __construct(PollFacade $facade) - { - $this->facade = $facade; + public function __construct( + private PollFacade $facade, + ) { } public function create(int $id): PollControl @@ -343,24 +359,21 @@ class PollControlFactory } ``` -Now we register our service to DI container to configuration: +We register this factory in our container in the configuration: ```neon services: - PollControlFactory ``` -Finally, we will use this factory in our presenter: +and finally, we use it in our presenter: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { - /** @var PollControlFactory */ - private $pollControlFactory; - - public function __construct(PollControlFactory $pollControlFactory) - { - $this->pollControlFactory = $pollControlFactory; + public function __construct( + private PollControlFactory $pollControlFactory, + ) { } protected function createComponentPollControl(): PollControl @@ -371,7 +384,7 @@ class PollPresenter extends Nette\UI\Application\Presenter } ``` -The great thing is that Nette DI can [generate |dependency-injection:factory] such simple factories, so instead of writing the whole code, you just need to write its interface: +What's great is that Nette DI can [generate |dependency-injection:factory] such simple factories, so instead of writing its entire code, you just need to write its interface: ```php interface PollControlFactory @@ -380,21 +393,21 @@ interface PollControlFactory } ``` -That's all. Nette internally implements this interface and injects it to our presenter, where we can use it. It also magically passes our parameter `$id` and instance of class `PollFacade` into our component. +And that's all. Nette internally implements this interface and injects it into the presenter, where we can use it. It magically adds the `$id` parameter and an instance of the `PollFacade` class to our component. Components in Depth =================== -Components in a Nette Application are the reusable parts of a web application that we embed in pages, which is the subject of this chapter. What exactly are the capabilities of such a component? +Components in Nette Application represent reusable parts of a web application that we embed into pages, and which this entire chapter is dedicated to. What exactly are the capabilities of such a component? -1) it is renderable in a template -2) it knows which part of itself to render during an [AJAX request |ajax#invalidation] (snippets) -3) it has the ability to store its state in a URL (persistence parameters) -4) has the ability to respond to user actions (signals) -5) creates a hierarchical structure (where the root is the presenter) +1) It is renderable in a template +2) It knows [which part of itself |ajax#Snippets] to render during an AJAX request (snippets) +3) It has the ability to store its state in the URL (persistent parameters) +4) It has the ability to react to user actions (signals) +5) It creates a hierarchical structure (where the root is the presenter) -Each of these functions is handled by one of the inheritance lineage classes. Rendering (1 + 2) is handled by [api:Nette\Application\UI\Control], incorporation into the [lifecycle |presenters#life-cycle-of-presenter] (3, 4) by the [api:Nette\Application\UI\Component] class, and the creation of the hierarchical structure (5) by the [Container and Component |component-model:] classes. +Each of these functions is handled by one of the classes in the inheritance line. Rendering (1 + 2) is handled by [api:Nette\Application\UI\Control], integration into the [lifecycle |presenters#Presenter Life Cycle] (3, 4) by the [api:Nette\Application\UI\Component] class, and the creation of the hierarchical structure (5) by the [Container and Component |component-model:] classes. ``` Nette\ComponentModel\Component { IComponent } @@ -409,30 +422,57 @@ Nette\ComponentModel\Component { IComponent } ``` -Life Cycle of Component ------------------------ +Component Lifecycle +------------------- + +[* lifecycle-component.svg *] *** *Component lifecycle* .<> + + +Validation of Persistent Parameters +----------------------------------- + +The values of [#persistent parameters] received from URLs are written to properties by the `loadState()` method. It also checks whether the data type specified for the property matches; otherwise, it responds with a 404 error and the page is not displayed. + +Never blindly trust persistent parameters, as they can be easily overwritten by the user in the URL. This is how we check, for example, if the page number `$this->page` is greater than 0. A suitable way is to override the mentioned `loadState()` method: + +```php +class PaginatingControl extends Control +{ + #[Persistent] + public int $page = 1; + + public function loadState(array $params): void + { + parent::loadState($params); // $this->page is set here + // follows the custom value check: + if ($this->page < 1) { + $this->error(); + } + } +} +``` -[* lifecycle-component.svg *] *** *Life cycle of component* .<> +The opposite process, i.e., collecting values from persistent properties, is handled by the `saveState()` method. Signals in Depth ---------------- -A signal causes a page reload like the original request (with the exception of AJAX) and invokes the method `signalReceived($signal)` whose default implementation in class `Nette\Application\UI\Component` tries to call a method composed of the words `handle{Signal}`. Further processing relies on the given object. Objects which are descendants of `Component` (i.e. `Control` and `Presenter`) try to call `handle{Signal}` with relevant parameters. +A signal causes the page to reload exactly like the original request (except when called via AJAX) and invokes the `signalReceived($signal)` method, whose default implementation in the `Nette\Application\UI\Component` class attempts to call a method composed of the words `handle{Signal}`. Further processing is up to the given object. Objects inheriting from `Component` (i.e., `Control` and `Presenter`) react by trying to call the `handle{Signal}` method with the appropriate parameters. -In other words: the definition of the method `handle{Signal}` is taken and all parameters which were received in the request are matched with the method's parameters. It means that the parameter `id` from the URL is matched to the method's parameter `$id`, `something` to `$something` and so on. And if the method doesn't exist, the method `signalReceived` throws [an exception |api:Nette\Application\UI\BadSignalException]. +In other words: the definition of the `handle{Signal}` function is taken, along with all parameters that came with the request, and parameters from the URL are assigned to the arguments by name, and an attempt is made to call the method. For example, the value from the `id` parameter in the URL is passed as the `$id` argument, `something` from the URL is passed as `$something`, etc. And if the method does not exist, the `signalReceived` method throws an [exception |api:Nette\Application\UI\BadSignalException]. -Signal can be received by any component, presenter of object which implements interface `SignalReceiver` if it's connected to component tree. +A signal can be received by any component, presenter, or object that implements the `SignalReceiver` interface and is connected to the component tree. -The main receivers of signals are `Presenters` and visual components extending `Control`. A signal is a sign for an object that it has to do something - poll counts in a vote from user, box with news has to unfold, form was sent and has to process data and so on. +The main recipients of signals will be `Presenters` and visual components inheriting from `Control`. A signal is intended to serve as a sign for an object that it should do something – a poll should count a vote from the user, a news block should expand and display twice as many news items, a form has been submitted and should process data, and so on. -The URL for the signal is created using the method [Component::link() |api:Nette\Application\UI\Component::link()]. As parameter `$destination` we pass string `{signal}!` and as `$args` an array of arguments which we want to pass to the signal handler. Signal parameters are attached to the URL of the current presenter/view. **The parameter `?do` in the URL determines the signal called.** +The URL for a signal is created using the [Component::link() |api:Nette\Application\UI\Component::link()] method. As the `$destination` parameter, we pass the string `{signal}!` and as `$args`, an array of arguments we want to pass to the signal. The signal is always called on the current presenter and action with the current parameters; the signal parameters are just added. Additionally, the **parameter `?do` which specifies the signal** is added right at the beginning. -Its format is `{signal}` or `{signalReceiver}-{signal}`. `{signalReceiver}` is the name of the component in the presenter. This is why hyphen (inaccurately dash) can't be present in the name of components - it is used to divide the name of the component and signal, but it's possible to compose several components. +Its format is either `{signal}` or `{signalReceiver}-{signal}`. `{signalReceiver}` is the name of the component in the presenter. Therefore, a hyphen cannot be used in the component name – it is used to separate the component name and the signal, although it is possible to nest multiple components this way. -The method [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] verifies whether a component (first argument) is a receiver of a signal (second argument). The second argument can be omitted - then it finds out if the component is a receiver of any signal. If the second parameter is `true` it verifies whether the component or its descendants are receivers of a signal. +The [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] method checks whether the component (first argument) is the recipient of the signal (second argument). The second argument can be omitted – then it checks if the component is the recipient of any signal. If the second parameter is set to `true`, it verifies whether the specified component or any of its descendants is the recipient. -In any phase preceding `handle{Signal}` can be signal performed manually by calling the method [processSignal()|api:Nette\Application\UI\Presenter::processSignal()] which takes responsibility for signal execution. Takes receiver component (if not set it is presenter itself) and sends it the signal. +At any stage preceding `handle{Signal}`, we can execute the signal manually by calling the [processSignal()|api:Nette\Application\UI\Presenter::processSignal()] method, which takes care of handling the signal – it takes the component identified as the signal recipient (if no recipient is specified, it is the presenter itself) and sends the signal to it. Example: @@ -442,4 +482,4 @@ if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, ' } ``` -The signal is executed prematurely and it won't be called again. +This executes the signal prematurely, and it will not be called again. diff --git a/application/en/configuration.texy b/application/en/configuration.texy index 449e2a15cb..a7c91cb890 100644 --- a/application/en/configuration.texy +++ b/application/en/configuration.texy @@ -1,8 +1,8 @@ -Configuring Application -*********************** +Application Configuration +************************* .[perex] -Overview of configuration options for the Nette Application. +Overview of configuration options for Nette Application. Application @@ -10,41 +10,54 @@ Application ```neon application: - # shows "Nette Application" panel in Tracy BlueScreen? + # show the "Nette Application" panel in Tracy BlueScreen? debugger: ... # (bool) defaults to true - # will error-presenter be called on error? - catchExceptions: ... # (bool) defaults to true in production mode + # will the error-presenter be called on error? + # effective only in development mode + catchExceptions: ... # (bool) defaults to true - # name of error-presenter - errorPresenter: Error # (string) defaults to 'Nette:Error' + # name of the error-presenter + errorPresenter: Error # (string|array) defaults to 'Nette:Error' - # defines the rules for resolving the presenter name to a class + # defines aliases for presenters and actions + aliases: ... + + # defines the rules for translating the presenter name to a class mapping: ... - # do bad links generate warnings? - # has effect only in developer mode + # suppress warnings for invalid links? + # effective only in development mode silentLinks: ... # (bool) defaults to false ``` -Because error-presenters are not called by default in development mode and the errors are displayed by Tracy, changing the value `catchExceptions` to `true` helps to verify that error-presenters works correct during development. +Since `nette/application` version 3.2, it is possible to define a pair of error presenters: + +```neon +application: + errorPresenter: + 4xx: Error4xx # for Nette\Application\BadRequestException + 5xx: Error5xx # for other exceptions +``` + +The `silentLinks` option determines how Nette behaves in development mode when link generation fails (for example, because the presenter does not exist, etc.). The default value `false` means that Nette triggers an `E_USER_WARNING` error. Setting it to `true` suppresses this error message. In a production environment, `E_USER_WARNING` is always triggered. This behavior can also be influenced by setting the presenter variable [$invalidLinkMode |creating-links#Invalid Links]. -Option `silentLinks` determines how Nette behaves in developer mode when link generation fails (for example, because there is no presenter, etc). The default value `false` means that Nette triggers `E_USER_WARNING`. Setting to `true` suppresses this error message. In a production environment, `E_USER_WARNING` is always invoked. We can also influence this behavior by setting the presenter variable [$invalidLinkMode |creating-links#Invalid Links]. +[Aliases simplify referencing |creating-links#Aliases] frequently used presenters. -The [mapping defines the rules |modules#mapping] by which the class name is derived from the presenter name. +The [mapping defines the rules |directory-structure#Presenter Mapping] by which the class name is derived from the presenter name. Automatic Registration of Presenters ------------------------------------ -Nette automatically adds presenters as services to the DI container, which significantly speeds up their creation. How Nette finds out presenters can be configured: +Nette automatically adds presenters as services to the DI container, which significantly speeds up their creation. How Nette locates presenters can be configured: ```neon application: - # to look for presenters in Composer class map? + # look for presenters in Composer class map? scanComposer: ... # (bool) defaults to true - # a mask that must match the class and file name + # a mask that the class and file name must match scanFilter: ... # (string) defaults to '*Presenter' # in which directories to look for presenters? @@ -52,7 +65,7 @@ application: - %vendorDir%/mymodule ``` -The directories listed in `scanDirs` do not override the default value `%appDir%`, but complement it, so `scanDirs` will contain both paths `%appDir%` and `%vendorDir%/mymodule`. If we want to overwrite the default directory, we use [exclamation mark |dependency-injection:configuration#Merging]: +The directories listed in `scanDirs` do not override the default value `%appDir%`, but complement it, so `scanDirs` will contain both paths `%appDir%` and `%vendorDir%/mymodule`. If we want to omit the default directory, we use an [exclamation mark |dependency-injection:configuration#Merging]: ```neon application: @@ -60,46 +73,52 @@ application: - %vendorDir%/mymodule ``` -Directory scanning can be turned off by setting false. We do not recommend completely suppressing the automatic addition of presenters, otherwise application performance will be reduced. +Directory scanning can be turned off by setting the value to false. We do not recommend completely suppressing the automatic addition of presenters, as this will reduce application performance. -Latte -===== +Latte Templates +=============== This setting globally affects the behavior of Latte in components and presenters. ```neon latte: - # shows Latte panel in the Tracy Bar for the main template (true) or for all components (all)? + # show the Latte panel in the Tracy Bar for the main template (true) or for all components (all)? debugger: ... # (true|false|'all') defaults to true - # switches Latte to XHTML mode (deprecated) - xhtml: ... # (bool) defaults to false - - # generates templates with declare(strict_types=1) + # generate templates with declare(strict_types=1) header strictTypes: ... # (bool) defaults to false - # class of $this->template + # enable [strict parser mode |latte:develop#strict mode] + strictParsing: ... # (bool) default is false + + # enable [checking of generated code |latte:develop#Checking Generated Code] + phpLinter: ... # (string) default is null + + # set the locale + locale: cs_CZ # (string) default is null + + # class of the $this->template object templateClass: App\MyTemplateClass # defaults to Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -If you are using Latte version 3, you can add new [extension |latte:creating-extension] using: +If you are using Latte version 3, you can add new [extensions |latte:extending-latte#Latte Extension] using: ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -If you are using Latte version 2, you can register new tags either by entering the class name or by referring to the service. Method `install()` is called by default, but this can be changed by specifying the name of another method: +If you are using Latte version 2, you can register new tags either by specifying the class name or by referencing a service. By default, the `install()` method is called, but this can be changed by specifying the name of another method: ```neon latte: - # registration of user Latte tags + # registration of custom Latte tags macros: - App\MyLatteMacros::register # static method, classname or callable - - @App\MyLatteMacrosFactory # service with install method - - @App\MyLatteMacrosFactory::register # service with register method + - @App\MyLatteMacrosFactory # service with install() method + - @App\MyLatteMacrosFactory::register # service with register() method services: - App\MyLatteMacrosFactory @@ -113,14 +132,14 @@ Basic settings: ```neon routing: - # shows routing panel in Tracy Bar? + # show the routing panel in Tracy Bar? debugger: ... # (bool) defaults to true - # to serialize router to DI container? + # serialize the router into the DI container cache: ... # (bool) defaults to false ``` -Router is usually defined in the [RouterFactory |routing#Route Collection] class. Alternatively, routs can also be defined in the configuration using `mask: action` pairs, but this method does not offer such a wide variation in settings: +Routing is usually defined in the [RouterFactory |routing#Route Collection] class. Alternatively, routes can also be defined in the configuration using `mask: action` pairs, but this method does not offer much flexibility: ```neon routing: @@ -137,10 +156,10 @@ Creating PHP constants. ```neon constants: - FOOBAR: 'baz' + Foobar: 'baz' ``` -The `FOOBAR` constant will created after startup. +The `Foobar` constant will be created after the application starts. .[note] Constants should not serve as globally available variables. To pass values to objects, use [dependency injection |dependency-injection:passing-dependencies]. @@ -149,9 +168,24 @@ Constants should not serve as globally available variables. To pass values to ob PHP === -You can set PHP directives. An overview of all directives can be found at [php.net |https://www.php.net/manual/en/ini.list.php]. +Setting PHP directives. An overview of all directives can be found at [php.net |https://www.php.net/manual/en/ini.list.php]. ```neon php: date.timezone: Europe/Prague ``` + + +DI Services +=========== + +These services are added to the DI container: + +| Name | Type | Description +|----------------------------|---------------------------------------------------|----------------------------------------- +| `application.application` | [api:Nette\Application\Application] | the [application runner |how-it-works#Nette Application] +| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] +| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | presenter factory +| `application.###` | [api:Nette\Application\UI\Presenter] | individual presenters +| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | factory for `Latte\Engine` object +| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | factory for [`$this->template` |templates] diff --git a/application/en/creating-links.texy b/application/en/creating-links.texy index a35ab0cb87..45ec9bd73e 100644 --- a/application/en/creating-links.texy +++ b/application/en/creating-links.texy @@ -3,158 +3,158 @@ Creating URL Links
    -Creating links in Nette is as easy as pointing a finger. Just point and the framework will do all the work for you. We will show: +Creating links in Nette is as simple as pointing a finger. Just aim, and the framework will do all the work for you. We will show: - how to create links in templates and elsewhere - how to distinguish a link to the current page -- what about invalid links +- what to do with invalid links
    -Thanks to [bidirectional routing|routing], you'll never have to hardcode application's URLs in the templates or code, which may change later or be complicated to compose. Just specify the presenter and the action in the link, pass any parameters and the framework will generate the URL itself. In fact, it's very similar to calling a function. You will like it. +Thanks to [bidirectional routing |routing], you will never have to hardcode URLs of your application into templates or code, which might change later or be complicated to assemble. In the link, just specify the presenter and action, pass any parameters, and the framework will generate the URL itself. Actually, it's very similar to calling a function. You'll like this. In the Presenter Template ========================= -Most often we create links in templates and a great helper is the attribute `n:href`: +Most often, we create links in templates, and the `n:href` attribute is a great helper: ```latte detail ``` -Note, that instead of the HTML attribute `href` we've used [n:attribute |latte:syntax#n:attributes] `n:href`. Its value isn't a URL, as you are used to with the `href` attribute, but name of the presenter and the action. +Notice that instead of the HTML attribute `href`, we used the [n:attribute |latte:syntax#n:attributes] `n:href`. Its value is not a URL, as would be the case with the `href` attribute, but the name of the presenter and action. -Clicking on a link is, simply said, something like calling a method `ProductPresenter::renderShow()`. And if it has parameters in its signature, we can call it with arguments: +Clicking on a link is, simply put, something like calling the `ProductPresenter::renderShow()` method. And if it has parameters in its signature, we can call it with arguments: ```latte -detail +product detail ``` -It is also possible to pass named parameters. The following link passes parameter `lang` with value `en`: +It is also possible to pass named parameters. The following link passes the parameter `lang` with the value `en`: ```latte -detail +product detail ``` -If method `ProductPresenter::renderShow()` does not have `$lang` in its signature, it can read the value of the parameter using `$lang = $this->getParameter('lang')`. +If the `ProductPresenter::renderShow()` method does not have `$lang` in its signature, it can retrieve the parameter's value using `$lang = $this->getParameter('lang')` or from a [property |presenters#Request Parameters]. -If the parameters are stored in an array, they can be expanded with the `...` operator (or `(expand)` operator in Latte 2.x): +If the parameters are stored in an array, they can be expanded using the `...` operator (or the `(expand)` operator in Latte 2.x): ```latte {var $args = [$product->id, lang => en]} -detail +product detail ``` -The so-called [persistent parameters|presenters#persistent parameters] are also automatically passed in the links. +So-called [persistent parameters |presenters#Persistent Parameters] are also automatically passed in links. -Attribute `n:href` is very handy for HTML tags ``. If we want to print the link elsewhere, for example in the text, we use `{link}`: +The `n:href` attribute is very handy for HTML `` tags. If we want to print the link elsewhere, for example in text, we use `{link}`: ```latte -URL is: {link Homepage:default} +URL is: {link Home:default} ``` In the Code =========== -The method `link()` is used to create a link in the presenter: +The `link()` method is used to create a link in the presenter: ```php $url = $this->link('Product:show', $product->id); ``` -Parameters can also be passed as an array where named parameters can also be specified: +Parameters can also be passed as an array, where named parameters can also be specified: ```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); +$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); ``` -Links can be created without a presenter too, using the [#LinkGenerator] and its method `link()`. +Links can also be created without a presenter, using the [#LinkGenerator] and its `link()` method. Links to Presenter ================== -If the target of the link is presenter and action, it has this syntax: +If the target of the link is a presenter and action, it has this syntax: ``` [//] [[[[:]module:]presenter:]action | this] [#fragment] ``` -The format is supported by all Latte tags and all presenter methods that work with links, ie `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` and also [#LinkGenerator]. So even if `n:href` is used in the examples, there could be any of the functions. +This format is supported by all Latte tags and all presenter methods that work with links, i.e., `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()`, and also [#LinkGenerator]. So even if `n:href` is used in the examples, any of these functions could be there. The basic form is therefore `Presenter:action`: ```latte -homepage +home page ``` -If we link to the action of the current presenter, we can omit its name: +If we are linking to an action of the current presenter, we can omit its name: ```latte -homepage +home page ``` -If the action is `default`, we can omit it, but the colon must remain: +If the target action is `default`, we can omit it, but the colon must remain: ```latte -homepage +home page ``` -Links may also point to other [modules]. Here, the links are distinguished into relative to the submodules, or absolute. The principle is analogous to disk paths, only instead of slashes there are colons. Let's assume that the actual presenter is part of module `Front`, then we will write: +Links can also point to other [modules |directory-structure#Presenters and Templates]. Here, links are distinguished as relative to a nested submodule, or absolute. The principle is analogous to disk paths, only colons are used instead of slashes. Assuming the current presenter is part of the `Front` module, we would write: ```latte link to Front:Shop:Product:show link to Admin:Product:show ``` -A special case is [linking to itself|#Links to Current Page]. Here we'll write `this` as the target. +A special case is a link [to itself |#Link to Current Page], where we specify `this` as the target. ```latte refresh ``` -We can link to a certain part of the HTML page via a so-called fragment after the `#` hash symbol: +We can link to a specific part of the page via a so-called fragment after the hash sign `#`: ```latte -link to Homepage:default and fragment #main +link to Home:default and fragment #main ``` Absolute Paths ============== -Links generated by `link()` or `n:href` are always absolute paths (i.e., they start with `/`), but not absolute URLs with a protocol and domain like `https://domain`. +Links generated using `link()` or `n:href` are always absolute paths (i.e., they start with `/`), but not absolute URLs with protocol and domain like `https://domain`. -To generate an absolute URL, add two slashes to the beginning (e.g., `n:href="//Homepage:"`). Or you can switch the presenter to generate only absolute links by setting `$this->absoluteUrls = true`. +To generate an absolute URL, add two slashes at the beginning (e.g., `n:href="//Home:"`). Alternatively, you can switch the presenter to generate only absolute links by setting `$this->absoluteUrls = true`. Link to Current Page ==================== -The target `this` will create a link to the current page: +The target `this` creates a link to the current page: ```latte refresh ``` -At the same time, all parameters specified in the signature of the `render()` or `action()` method are transferred. So if we are on the `Product:show` and `id:123` pages, the link to `this` will also pass this parameter. +At the same time, all parameters specified in the signature of the `action()` or `render()` method are transferred (if `action()` is not defined). So if we are on the `Product:show` page with `id: 123`, the link to `this` will also pass this parameter. -Of course, it is possible to specify the parameters directly: +Of course, it is possible to specify parameters directly: ```latte refresh ``` -Function `isLinkCurrent()` determines if the target of the link is the same as the current page. This can be used, for example, in a template to differentiate links, etc. +The `isLinkCurrent()` function checks if the link target is identical to the current page. This can be used, for example, in a template to distinguish links, etc. -The parameters are the same as for the `link()` method, but it is also possible to use the wildcard `*` instead of a specific action, which means any action of the presenter. +The parameters are the same as for the `link()` method, but it is also possible to use the wildcard `*` instead of a specific action, which means any action of the given presenter. ```latte {if !isLinkCurrent('Admin:login')} - Přihlaste se + Login {/if}
  • @@ -162,18 +162,18 @@ The parameters are the same as for the `link()` method, but it is also possible
  • ``` -An abbreviated form can be used in combination with `n:href` in single element: +In combination with `n:href` in a single element, a shorthand form can be used: ```latte -... +... ``` -Wildcard character `*` replaces presenter's action only, not presenter itself. +The wildcard `*` can only be used instead of the action, not the presenter. -To find out if we are in a certain module or its submodule we can use `isModuleCurrent(moduleName)` function. +To determine if we are in a specific module or its submodule, use the `isModuleCurrent(moduleName)` method. ```latte -
  • +
  • ...
  • ``` @@ -182,19 +182,19 @@ To find out if we are in a certain module or its submodule we can use `isModuleC Links to Signal =============== -The target of the link may not only be the presenter and action, but also the [signal |components#Signal] (they call the method `handle()`). The syntax is as follows: +The target of a link doesn't have to be just a presenter and action, but also a [signal |components#Signal] (they call the `handle()` method). Then the syntax is as follows: ``` [//] [sub-component:]signal! [#fragment] ``` -The signal is therefore distinguishes by exclamation mark: +The signal is thus distinguished by an exclamation mark: ```latte signal ``` -You can also create a link to the signal of the subcomponent (or sub-subcomponent): +You can also create a link to a signal of a subcomponent (or sub-subcomponent): ```latte signal @@ -204,36 +204,60 @@ You can also create a link to the signal of the subcomponent (or sub-subcomponen Links in Component ================== -Because [components] are separate reusable units that should have no relations to surrounding presenters, the links work a little differently. The Latte attribute `n:href` and tag `{link}` and component methods such as `link()` and others always consider the target **as the signal name**. Therefore it is not necessary to use an exclamation mark: +Because [components|components] are separate reusable units that should not have any ties to surrounding presenters, links work a bit differently here. The Latte attribute `n:href` and the tag `{link}`, as well as component methods like `link()` and others, **always consider the link target as the signal name**. Therefore, it is not even necessary to include an exclamation mark: ```latte signal, not an action ``` -If we want to link to presenters in the component template, we use the tag `{plink}`: +If we wanted to link to presenters in the component template, we would use the `{plink}` tag: ```latte -homepage +home ``` or in the code ```php -$this->getPresenter()->link('Homepage:default') +$this->getPresenter()->link('Home:default') ``` +Aliases .{data-version:v3.2.2} +============================== + +Sometimes it can be useful to assign an easily memorable alias to a Presenter:action pair. For example, naming the homepage `Front:Home:default` simply as `home` or `Admin:Dashboard:default` as `admin`. + +Aliases are defined in the [configuration|configuration] under the key `application › aliases`: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +In links, they are then written using an at sign, for example: + +```latte +administration +``` + +They are also supported in all methods that work with links, such as `redirect()` and similar. + + Invalid Links ============= -It may happen that we create an invalid link - either because it refers to a non-existing presenter, or because it passes more parameters that the target method receives in its signature, or when there can't be a generated URL for the targeted action. What to do with invalid links is determined by the static variable `Presenter::$invalidLinkMode`. It can have one of these values (constants): +It may happen that we create an invalid link - either because it leads to a non-existent presenter, or because it passes more parameters than the target method accepts in its signature, or when a URL cannot be generated for the target action. How to handle invalid links is determined by the static variable `Presenter::$invalidLinkMode`. It can take a combination of these values (constants): -- `Presenter::INVALID_LINK_SILENT` - silent mode, returns symbol `#` as URL -- `Presenter::INVALID_LINK_WARNING` - E_USER_WARNING will be produced -- `Presenter::INVALID_LINK_TEXTUAL` - visual warning, the error text is displayed in the link -- `Presenter::INVALID_LINK_EXCEPTION` - InvalidLinkException will be thrown +- `Presenter::InvalidLinkSilent` - silent mode, returns the character # as the URL +- `Presenter::InvalidLinkWarning` - an E_USER_WARNING warning is thrown, which will be logged in production mode, but will not interrupt script execution +- `Presenter::InvalidLinkTextual` - visual warning, prints the error directly into the link +- `Presenter::InvalidLinkException` - throws InvalidLinkException -The default setup in production mode is `INVALID_LINK_WARNING` and in development mode is `INVALID_LINK_WARNING | INVALID_LINK_TEXTUAL`. `INVALID_LINK_WARNING` doesn't kill the script in the production environment, but the warning will be logged. In the development environment, [Tracy |tracy:] will intercept the warning and display the error bluescreen. If the `INVALID_LINK_TEXTUAL` is set, presenter and components return error message as URL which stars with `#error:`. To make such links visible, we can add a CSS rule to our stylesheet: +The default setting is `InvalidLinkWarning` in production mode and `InvalidLinkWarning | InvalidLinkTextual` in development mode. `InvalidLinkWarning` in the production environment does not cause script interruption, but the warning will be logged. In the development environment, [Tracy |tracy:] catches it and displays a bluescreen. `InvalidLinkTextual` works by returning an error message as the URL, starting with the characters `#error:`. To make such links noticeable at first glance, add the following to your CSS: ```css a[href^="#error:"] { @@ -242,7 +266,7 @@ a[href^="#error:"] { } ``` -If we don't want warnings to be produced in the development environment we can turn on silent invalid link mode in the [configuration]. +If we do not want warnings to be produced in the development environment, we can set the silent mode directly in the [configuration|configuration]. ```neon application: @@ -253,10 +277,10 @@ application: LinkGenerator ============= -How to create links with the method `link()` comfort, but without the presence of a presenter? That's why here is [api:Nette\Application\LinkGenerator]. +How to create links with similar comfort as the `link()` method, but without the presence of a presenter? That's what [api:Nette\Application\LinkGenerator] is for. -LinkGenerator is a service that you can have passed through the constructor and then create links using its method `link()`. +LinkGenerator is a service that you can have passed via the constructor and then create links using its `link()` method. -There is a difference compared to presenters. LinkGenerator creates all links as absolute URLs. Furthermore, there is no "current presenter", so it is not possible to specify only the name of the action `link('default')` or the relative paths to the [modules]. +There is a difference compared to presenters. LinkGenerator creates all links directly as absolute URLs. Furthermore, there is no "current presenter", so it is not possible to specify only the action name `link('default')` as the target or use relative paths to modules. Invalid links always throw `Nette\Application\UI\InvalidLinkException`. diff --git a/application/en/directory-structure.texy b/application/en/directory-structure.texy new file mode 100644 index 0000000000..a94b6148c7 --- /dev/null +++ b/application/en/directory-structure.texy @@ -0,0 +1,526 @@ +Directory Structure of the Application +************************************** + +
    + +How to design a clear and scalable directory structure for projects in Nette Framework? We will show you proven practices that will help you organize your code. You will learn: + +- how to **logically structure** the application into directories +- how to design the structure so that it **scales well** as the project grows +- what are the **possible alternatives** and their advantages or disadvantages + +
    + + +It is important to mention that Nette Framework itself does not enforce any specific structure. It is designed to be easily adaptable to any needs and preferences. + + +Basic Project Structure +======================= + +Although Nette Framework does not dictate any fixed directory structure, there is a proven default arrangement in the form of the [Web Project|https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← application directory +├── assets/ ← SCSS, JS files, images..., alternatively resources/ +├── bin/ ← scripts for command line +├── config/ ← configuration +├── log/ ← logged errors +├── temp/ ← temporary files, cache +├── tests/ ← tests +├── vendor/ ← libraries installed by Composer +└── www/ ← public directory (document-root) +\-- + +You can modify this structure freely according to your needs - rename or move folders. Then you just need to adjust the relative paths to directories in `Bootstrap.php` and possibly `composer.json`. Nothing more is needed, no complex reconfiguration, no changes to constants. Nette has smart autodetection and automatically recognizes the application's location, including its base URL. + + +Code Organization Principles +============================ + +When you first explore a new project, you should be able to quickly orient yourself. Imagine clicking on the `app/Model/` directory and seeing this structure: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +From this, you only learn that the project uses some services, repositories, and entities. You learn nothing about the actual purpose of the application. + +Let's look at a different approach - **organization by domains**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Here it's different - at first glance, it's clear that this is an e-shop. The directory names themselves reveal what the application can do - it works with payments, orders, and products. + +The first approach (organization by class type) brings several problems in practice: code that is logically related is fragmented across different folders, and you have to jump between them. Therefore, we will organize by domains. + + +Namespaces +---------- + +It is customary for the directory structure to correspond to the namespaces in the application. This means that the physical location of files matches their namespace. For example, a class located in `app/Model/Product/ProductRepository.php` should have the namespace `App\Model\Product`. This principle helps in navigating the code and simplifies autoloading. + + +Singular vs Plural in Names +--------------------------- + +Notice that we use singular for the main application directories: `app`, `config`, `log`, `temp`, `www`. The same applies inside the application: `Model`, `Core`, `Presentation`. This is because each represents a single cohesive concept. + +Similarly, `app/Model/Product` represents everything related to products. We don't call it `Products` because it's not a folder full of products (that would contain files like `nokia.php`, `samsung.php`). It's a namespace containing classes for working with products - `ProductRepository.php`, `ProductService.php`. + +The folder `app/Tasks` is plural because it contains a set of separate executable scripts - `CleanupTask.php`, `ImportTask.php`. Each of them is an independent unit. + +For consistency, we recommend using: +- Singular for namespaces representing a functional unit (even if working with multiple entities) +- Plural for collections of independent units +- In case of uncertainty, or if you don't want to think about it, choose singular + + +Public Directory `www/` +======================= + +This directory is the only one accessible from the web (the document-root). You might often encounter the name `public/` instead of `www/` - it's just a matter of convention and does not affect the application's functionality. The directory contains: +- Application [entry point |bootstrapping#index.php] `index.php` +- `.htaccess` file with rules for mod_rewrite (for Apache) +- Static files (CSS, JavaScript, images) +- Uploaded files + +For proper application security, it is crucial to have the [document-root configured correctly |nette:troubleshooting#How to Change or Remove www Directory from URL]. + +.[note] +Never place the `node_modules/` folder in this directory - it contains thousands of files that might be executable and should not be publicly accessible. + + +Application Directory `app/` +============================ + +This is the main directory containing the application code. Basic structure: + +/--pre +app/ +├── Core/ ← infrastructure concerns +├── Model/ ← business logic +├── Presentation/ ← presenters and templates +├── Tasks/ ← command scripts +└── Bootstrap.php ← application bootstrap class +\-- + +`Bootstrap.php` is the [application startup class|bootstrapping] that initializes the environment, loads configuration, and creates the DI container. + +Let's now look at the individual subdirectories in more detail. + + +Presenters and Templates +======================== + +The presentation part of the application is located in the `app/Presentation` directory. An alternative is the shorter `app/UI`. This is the place for all presenters, their templates, and any associated helper classes. + +We organize this layer according to domains. In a complex project combining an e-shop, blog, and API, the structure would look like this: + +/--pre +app/Presentation/ +├── Shop/ ← e-shop frontend +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administration +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← API endpoints + └── V1/ +\-- + +Conversely, for a simple blog, we would use the following structure: + +/--pre +app/Presentation/ +├── Front/ ← website frontend +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administration +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemaps, etc. +\-- + +Folders like `Home/` or `Dashboard/` contain presenters and templates. Folders like `Front/`, `Admin/`, or `Api/` are called **modules**. Technically, these are regular directories used for the logical partitioning of the application. + +Each folder containing a presenter includes the presenter file itself and its templates. For example, the `Dashboard/` folder contains: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presenter +└── default.latte ← template +\-- + +This directory structure is reflected in the class namespaces. For example, `DashboardPresenter` is located in the `App\Presentation\Admin\Dashboard` namespace (see [#Presenter Mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + // ... +} +``` + +We refer to the `Dashboard` presenter within the `Admin` module in the application using colon notation as `Admin:Dashboard`. Its `default` action is then referred to as `Admin:Dashboard:default`. For nested modules, we use multiple colons, for example, `Shop:Order:Detail:default`. + + +Flexible Structure Development +------------------------------ + +One of the great advantages of this structure is how elegantly it adapts to the growing needs of the project. As an example, let's take the part generating XML feeds. Initially, we have a simple form: + +/--pre +Export/ +├── ExportPresenter.php ← one presenter for all exports +├── sitemap.latte ← template for sitemap +└── feed.latte ← template for RSS feed +\-- + +Over time, more feed types are added, and we need more logic for them... No problem! The `Export/` folder simply becomes a module: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← feed for Amazon + └── ebay.latte ← feed for eBay +\-- + +This transformation is completely smooth - just create new subfolders, divide the code into them and update links (e.g. from `Export:feed` to `Export:Feed:amazon`). Thanks to this, we can gradually expand the structure as needed, the nesting level is not limited in any way. + +For example, if in the administration you have many presenters related to order management, such as `OrderDetail`, `OrderEdit`, `OrderDispatch`, etc., you can create a module (folder) named `Order` for better organization, which will contain (folders for) presenters `Detail`, `Edit`, `Dispatch`, and others. + + +Template Location +----------------- + +In the previous examples, we saw that templates are located directly in the folder with the presenter: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presenter +├── DashboardTemplate.php ← optional template class +└── default.latte ← template +\-- + +This location proves to be the most convenient in practice - you have all related files readily available. + +Alternatively, you can place templates in a `templates/` subfolder. Nette supports both variants. You can even place templates completely outside the `Presentation/` folder. Everything about template location options can be found in the chapter [Finding Templates |templates#Template Lookup]. + + +Helper Classes and Components +----------------------------- + +Presenters and templates often come with other helper files. We place them logically according to their scope: + +1. **Directly with the presenter** in the case of specific components for that presenter: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← component for product listing +└── FilterForm.php ← form for filtering +\-- + +2. **For the module** - we recommend using the `Accessory` folder, which is placed conveniently at the beginning alphabetically: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← components for frontend +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **For the entire application** - in `Presentation/Accessory/`: +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +Alternatively, you can place helper classes like `LatteExtension.php` or `TemplateFilters.php` in the infrastructure folder `app/Core/Latte/`. And components in `app/Components`. The choice depends on team conventions. + + +Model - Heart of the Application +================================ + +The model contains all the business logic of the application. The rule for its organization is again - structure by domains: + +/--pre +app/Model/ +├── Payment/ ← everything about payments +│ ├── PaymentFacade.php ← main entry point +│ ├── PaymentRepository.php +│ ├── Payment.php ← entity +├── Order/ ← everything about orders +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← everything about shipping +\-- + +In the model, you typically encounter these types of classes: + +**Facades**: represent the main entry point into a specific domain within the application. They act as an orchestrator, coordinating cooperation between various services to implement complete use-cases (like "create order" or "process payment"). Beneath its orchestration layer, the facade hides implementation details from the rest of the application, thereby providing a clean interface for working with the given domain. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // validation + // order creation + // email sending + // writing to statistics + } +} +``` + +**Services**: focus on specific business operations within a domain. Unlike facades, which orchestrate entire use-cases, a service implements specific business logic (like price calculations or payment processing). Services are typically stateless and can be used either by facades as building blocks for more complex operations or directly by other parts of the application for simpler tasks. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // price calculation + } +} +``` + +**Repositories**: handle all communication with the data storage, typically a database. Their task is to load and save entities and implement methods for searching them. A repository shields the rest of the application from the implementation details of the database and provides an object-oriented interface for working with data. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Entities**: objects representing the main business concepts in the application, which have their own identity and change over time. Typically, these are classes mapped to database tables using an ORM (like Nette Database Explorer or Doctrine). Entities can contain business rules related to their data and validation logic. + +```php +// Entity mapped to the 'orders' database table +class Order extends Nette\Database\Table\ActiveRow +{ + public function addItem(Product $product, int $quantity): void + { + $this->related('order_items')->insert([ + 'product_id' => $product->id, + 'quantity' => $quantity, + 'unit_price' => $product->price, + ]); + } +} +``` + +**Value Objects**: immutable objects representing values without their own identity - for example, a monetary amount or an email address. Two instances of a value object with the same values are considered identical. + + +Infrastructure Code +=================== + +The `Core/` folder (or alternatively `Infrastructure/`) is home to the technical foundation of the application. Infrastructure code typically includes: + +/--pre +app/Core/ +├── Router/ ← routing and URL management +│ └── RouterFactory.php +├── Security/ ← authentication and authorization +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← logging and monitoring +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← caching layer +│ └── FullPageCache.php +└── Integration/ ← integration with external services + ├── Slack/ + └── Stripe/ +\-- + +For smaller projects, a flat structure is naturally sufficient: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +This is code that: + +- Handles technical infrastructure (routing, logging, caching) +- Integrates external services (Sentry, Elasticsearch, Redis) +- Provides basic services for the entire application (mail, database) +- Is mostly independent of a specific domain - cache or logger works the same for an e-shop or a blog. + +Are you wondering whether a certain class belongs here or in the model? The key difference is that code in `Core/`: + +- Knows nothing about the domain (products, orders, articles) +- Can usually be transferred to another project +- Solves "how it works" (how to send an email), not "what it does" (which email to send) + +An example for better understanding: + +- `App\Core\MailerFactory` - creates instances of the class for sending emails, handles SMTP settings +- `App\Model\OrderMailer` - uses `MailerFactory` to send emails about orders, knows their templates and when they should be sent + + +Command Scripts +=============== + +Applications often need to perform activities outside of regular HTTP requests - whether it's background data processing, maintenance, or periodic tasks. Simple scripts in the `bin/` directory are used for execution, while the actual implementation logic is placed in `app/Tasks/` (or `app/Commands/`). + +Example: + +/--pre +app/Tasks/ +├── Maintenance/ ← maintenance scripts +│ ├── CleanupCommand.php ← deleting old data +│ └── DbOptimizeCommand.php ← database optimization +├── Integration/ ← integration with external systems +│ ├── ImportProducts.php ← import from supplier system +│ └── SyncOrders.php ← order synchronization +└── Scheduled/ ← regular tasks + ├── NewsletterCommand.php ← sending newsletters + └── ReminderCommand.php ← customer notifications +\-- + +What belongs in the model and what in command scripts? For example, the logic for sending a single email is part of the model, while the bulk sending of thousands of emails belongs in `Tasks/`. + +Tasks are usually [run from the command line |https://blog.nette.org/en/cli-scripts-in-nette-application] or via cron. They can also be run via an HTTP request, but security must be considered. The presenter that runs the task needs to be secured, for example, only for logged-in users or with a strong token and access from allowed IP addresses. For long-running tasks, it is necessary to increase the script time limit and use `session_write_close()` to avoid locking the session. + + +Other Possible Directories +========================== + +In addition to the mentioned basic directories, you can add other specialized folders according to project needs. Let's look at the most common ones and their use: + +/--pre +app/ +├── Api/ ← API logic independent of the presentation layer +├── Database/ ← migration scripts and seeders for test data +├── Components/ ← shared visual components across the entire application +├── Event/ ← useful if using an event-driven architecture +├── Mail/ ← email templates and related logic +└── Utils/ ← helper classes +\-- + +For shared visual components used in presenters across the application, you can use the `app/Components` or `app/Controls` folder: + +/--pre +app/Components/ +├── Form/ ← shared form components +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← components for data listings +│ └── DataGrid.php +└── Navigation/ ← navigation elements + ├── Breadcrumbs.php + └── Menu.php +\-- + +This is where components with more complex logic belong. If you want to share components between multiple projects, it is advisable to extract them into a separate Composer package. + +In the `app/Mail` directory, you can place email communication management: + +/--pre +app/Mail/ +├── templates/ ← email templates +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Presenter Mapping +================= + +Mapping defines the rules for deriving the class name from the presenter name. We specify them in the [configuration|configuration] under the key `application › mapping`. + +On this page, we have shown that we place presenters in the `app/Presentation` folder (or `app/UI`). We must inform Nette of this convention in the configuration file. One line is sufficient: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +How does mapping work? For a better understanding, let's first imagine an application without modules. We want the presenter classes to fall under the `App\Presentation` namespace, so that the `Home` presenter maps to the `App\Presentation\HomePresenter` class. This is achieved with this configuration: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +Mapping works by replacing the asterisk in the mask `App\Presentation\*Presenter` with the presenter name `Home`, resulting in the final class name `App\Presentation\HomePresenter`. Simple! + +However, as you see in the examples in this and other chapters, we place presenter classes in eponymous subdirectories, for example, the `Home` presenter maps to the class `App\Presentation\Home\HomePresenter`. We achieve this by using double asterisks `**` (requires Nette Application 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Now we proceed to mapping presenters into modules. We can define specific mapping for each module: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +According to this configuration, the presenter `Front:Home` maps to the class `App\Presentation\Front\Home\HomePresenter`, while the presenter `Api:OAuth` maps to the class `App\Api\OAuthPresenter`. + +Since the `Front` and `Admin` modules have a similar mapping pattern, and there will likely be more such modules, it is possible to create a general rule that replaces them. A new asterisk for the module is added to the class mask: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +It also works for deeper nested directory structures, such as the presenter `Admin:User:Edit`, where the segment with the asterisk repeats for each module level, resulting in the class `App\Presentation\Admin\User\Edit\EditPresenter`. + +An alternative notation is to use an array consisting of three segments instead of a string. This notation is equivalent to the previous one: + +```neon +application: + mapping: + *: [App\Presentation, *, **Presenter] + Api: [App\Api, '', *Presenter] +``` diff --git a/application/en/how-it-works.texy b/application/en/how-it-works.texy index 7546d79e79..93fa1655fd 100644 --- a/application/en/how-it-works.texy +++ b/application/en/how-it-works.texy @@ -3,10 +3,10 @@ How Do Applications Work?
    -You are currently reading the basic document of the Nette documentation. You will learn all the principle of web applications. Nice from A to Z, from the moment of birth until the last breath of the PHP script. After reading you will know: +You are currently reading the foundational chapter of the Nette documentation. You will learn the complete principles behind how web applications work, from A to Z, from the moment a request is born until the PHP script completes execution. After reading, you will understand: - how it all works -- what is Bootstrap, Presenter and DI container +- what Bootstrap, Presenter, and the DI container are - what the directory structure looks like
    @@ -15,190 +15,184 @@ You are currently reading the basic document of the Nette documentation. You wil Directory Structure =================== -Open a skeleton example of a web application called [WebProject|https://github.com/nette/web-project] and you can watch the files being written about. +Open the example skeleton of a web application called [WebProject|https://github.com/nette/web-project]. As you read, you can refer to the files being discussed. The directory structure looks something like this: /--pre web-project/ -├── app/ ← directory with application -│ ├── Presenters/ ← presenter classes -│ │ ├── HomepagePresenter.php ← Homepage presenter class -│ │ └── templates/ ← templates directory -│ │ ├── @layout.latte ← template of shared layout -│ │ └── Homepage/ ← templates for Homepage presenter -│ │ └── default.latte ← template for action `default` -│ ├── Router/ ← configuration of URL addresses +├── app/ ← application directory +│ ├── Core/ ← core classes necessary for operation +│ │ └── RouterFactory.php ← URL address configuration +│ ├── Presentation/ ← presenters, templates & co. +│ │ ├── @layout.latte ← layout template +│ │ └── Home/ ← Home presenter directory +│ │ ├── HomePresenter.php ← Home presenter class +│ │ └── default.latte ← template for default action │ └── Bootstrap.php ← booting class Bootstrap -├── bin/ ← scripts for the command line +├── bin/ ← scripts executed from the command line ├── config/ ← configuration files │ ├── common.neon -│ └── local.neon -├── log/ ← error logs +│ └── services.neon +├── log/ ← logged errors ├── temp/ ← temporary files, cache, … ├── vendor/ ← libraries installed by Composer │ ├── ... -│ └── autoload.php ← autoloading of libs installed by Composer -├── www/ ← public directory, document root of project -│ ├── .htaccess ← mod_rewrite rules etc +│ └── autoload.php ← autoloading of all installed packages +├── www/ ← public directory, document root of the project +│ ├── .htaccess ← mod_rewrite rules │ └── index.php ← initial file that launches the application └── .htaccess ← prohibits access to all directories except www \-- -You can change the directory structure in any way, rename or move folders, and then just edit the paths to `log/` and `temp/` in the `Bootstrap.php` file and the path to this file in `composer.json` in the `autoload` section. Nothing more, no complicated reconfiguration, no constant changes. Nette has a [smart autodetection|bootstrap#development-vs-production-mode]. +You can change the directory structure in any way, rename or move folders; it is completely flexible. Nette also features smart autodetection and automatically recognizes the application's location, including its URL base. -For slightly larger applications, we can divide folders with presenters and templates into subdirectories (on disk) and into namespaces (in code), which we call [modules]. +For slightly larger applications, we can organize presenter and template folders into [subdirectories |directory-structure#Presenters and Templates] and group classes into namespaces, which we call modules. -The `www/` directory is the public directory or document-root of the project. You can rename it without having to set anything else on the application side. You just need to [configure the hosting |nette:troubleshooting#How to change or remove www directory from URL] so that the document-root goes to this directory. +The `www/` directory represents the public directory or document-root of the project. You can rename it without needing to configure anything else on the application side. You just need to [configure the hosting |nette:troubleshooting#How to Change or Remove www Directory from URL] so that the document-root points to this directory. -You can also download the WebProject directly, including Nette, using [Composer |best-practices:composer]: +You can also download WebProject directly, including Nette, using [Composer |best-practices:composer]: ```shell composer create-project nette/web-project ``` -On Linux or macOS, set the [write permissions |nette:troubleshooting#Setting directory permissions] for directories `log/` and `temp/`. +On Linux or macOS, set [write permissions |nette:troubleshooting#Setting Directory Permissions] for the `log/` and `temp/` directories. -The WebProject application is ready to run, there is no need to configure anything else at all and you can view it directly in the browser by accessing the folder `www/`. +The WebProject application is ready to run; there is no need to configure anything at all, and you can view it directly in the browser by accessing the `www/` folder. HTTP Request ============ -It all begins when a user opens the page in a browser and browser knocks on the server with an HTTP request. The request goes to a PHP file located in the public directory `www/`, which is `index.php`. Let's suppose that this is a request to `https://example.com/product/123`. Thanks to the appropriate [server settings |nette:troubleshooting#How to configure a server for nice URLs?], this URL is also mapped to the `index.php` file and will be executed. +Everything starts when a user opens a page in their browser. The browser sends an HTTP request to the server. This request targets a single PHP file located in the public directory `www/`, which is `index.php`. Let's assume the request is for the address `https://example.com/product/123`. Thanks to appropriate [server configuration |nette:troubleshooting#How to Configure a Server for Nice URLs], even this URL is mapped to the `index.php` file, which is then executed. -Its task is: +Its task is to: 1) initialize the environment -2) get the factory -3) launch the Nette application that handles the request +2) obtain the factory +3) run the Nette application, which handles the request -What kind of factory? We do not produce tractors, but websites! Hold on, it'll be explained right away. +What factory? We're not producing tractors, we're building websites! Hold on, it will be explained shortly. -By "initialize the environment" we mean, for example, that [Tracy |tracy:] is activated, which is an amazing tool for logging or visualizing errors. It logs errors on the production server and displays them directly on the development server. Therefore, initialization also needs to decide whether the site is running in production or developer mode. To do this, Nette uses autodetection: if you run the site on localhost, it runs in developer mode. You don't have to configure anything and the application is ready for both development and production deployment. These steps are performed and described in detail in the chapter about [Bootstrap class |bootstrap]. +By 'environment initialization', we mean, for example, activating [Tracy|tracy:], which is an amazing tool for logging or visualizing errors. On a production server, it logs errors; in a development environment, it displays them directly. Thus, initialization also includes determining whether the site is running in production or development mode. Nette uses [smart autodetection |bootstrapping#Development vs Production Mode] for this: if you run the site on localhost, it operates in development mode. You don't need to configure anything, and the application is immediately ready for both development and live deployment. These steps are performed and described in detail in the chapter about the [Bootstrap class|bootstrapping]. -The third point (yes, we skipped the second, but we will return to it) is to start the application. The handling of HTTP requests in Nette is done by the class `Nette\Application\Application` (hereinafter referred to as the `Application`), so when we say "run an application", we mean to call a method with the name `run()` on an object of this class. +The third point (yes, we skipped the second, but we'll return to it) is launching the application. Handling HTTP requests in Nette is the responsibility of the `Nette\Application\Application` class (hereafter `Application`). So, when we say run the application, we specifically mean calling the aptly named `run()` method on an object of this class. -Nette is a mentor who guides you to write clean applications by proven methodologies. And the most proven is called **dependency injection**, abbreviated DI. At the moment we don't want to burden you with explaining DI, since there is a [separate chapter |dependency-injection:introduction], the important thing here is that the key objects will usually be created by a factory for objects called **DI container** (abbreviated DIC). Yes, this is the factory that was mentioned a while ago. And it also creates the `Application` object for us, so we need a container first. We get it using the `Configurator` class and let it produce `Application` object, call the method `run()` and this starts Nette application. This is exactly what happens in the [index.php |bootstrap#index.php] file. +Nette acts as a mentor, guiding you to write clean applications according to proven methodologies. One of the most established of these is **dependency injection**, abbreviated as DI. We don't want to burden you with explaining DI right now; there's a [dedicated chapter|dependency-injection:introduction] for that. The essential consequence is that key objects are typically created by an object factory known as the **DI container** (or DIC). Yes, this is the factory mentioned earlier. It also produces the `Application` object for us, which is why we need the container first. We obtain it using the `Configurator` class, let it create the `Application` object, call the `run()` method on it, and thus the Nette application starts. This is precisely what happens in the [index.php |bootstrapping#index.php] file. Nette Application ================= -The Application class has a single task: to respond to an HTTP request. +The `Application` class has a single task: to respond to the HTTP request. -Applications written in Nette are divided into many so-called presenters (in other frameworks you may come across the term controller, which is the same), which are classes representing a specific website page: eg homepage; product in e-shop; sign-in form; sitemap feed, etc. The application can have from one to thousands of presenters. +Applications written in Nette are divided into many so-called presenters (you might encounter the term 'controller' in other frameworks, which is essentially the same thing). These are classes, each representing a specific page of the website: e.g., the homepage, a product in an e-shop, a login form, a sitemap feed, etc. An application can have anywhere from one to thousands of presenters. -The application starts by asking the so-called router to decide which of the presenters to pass the current request for processing. The router decides whose responsibility it is. It looks at the input URL `https://example.com/product/123` and, based on how it is set up, decides that this is a job, for example, for **presenter** `Product`, who wants to `show` a product with `id: 123` as an action. It is a good habit to write a pairs of presenter + action separated by a colon as `Product:show`. +The `Application` starts by asking the so-called router to decide which presenter should handle the current request. The router determines the responsibility. It examines the input URL `https://example.com/product/123` and, based on its configuration, decides that this task belongs, for example, to the `Product` **presenter**, which should perform the `show` **action** for the product with `id: 123`. It's good practice to write the presenter + action pair separated by a colon, like `Product:show`. -So the router transformed the URL into a pair `Presenter:action` + parameters, in our case `Product:show` + `id: 123`. You can see how a router looks like in file `app/Router/RouterFactory.php` and we will describe it in detail in chapter [Routing]. +Thus, the router transformed the URL into the pair `Presenter:action` + parameters, in our case `Product:show` + `id: 123`. You can see what such a router looks like in the file `app/Core/RouterFactory.php`, and we describe it in detail in the [Routing |Routing] chapter. -Let's move on. The application already knows the name of the presenter and can continue. By creating an object `ProductPresenter`, which is the code of presenter `Product`. More precisely, it asks the DI container for creating the presenter, because producting objects is its job. +Let's move on. The `Application` now knows the name of the presenter and can proceed. It does this by creating an instance of the `ProductPresenter` class, which contains the code for the `Product` presenter. More precisely, it asks the DI container to create the presenter, because creating objects is its responsibility. The presenter might look like this: ```php class ProductPresenter extends Nette\Application\UI\Presenter { - private $repository; - - public function __construct(ProductRepository $repository) - { - $this->repository = $repository; + public function __construct( + private ProductRepository $repository, + ) { } public function renderShow(int $id): void { - // we obtain data from the model and pass it to the template + // obtain data from the model and pass it to the template $this->template->product = $this->repository->getProduct($id); } } ``` -The request is handled by the presenter. And the task is clear: do action `show` with `id: 123`. Which in the language of presenters means that the method `renderShow()` is called and in the parameter `$id` it gets `123`. +The presenter takes over handling the request. The task is clear: execute the `show` action with `id: 123`. In presenter terminology, this means the `renderShow()` method is called, receiving `123` in the `$id` parameter. -A presenter can handle multiple actions, ie have multiple methods `render()`. But we recommend designing presenters with one or as few actions as possible. +A presenter can handle multiple actions, meaning it can have multiple `render()` methods. However, we recommend designing presenters with one or as few actions as possible. -So, the method `renderShow(123)` was called, whose code is fictional example, but you can see on it how the data is passed to the template, ie by writing to `$this->template`. +So, the `renderShow(123)` method was called. Its code is a fictional example, but it demonstrates how data is passed to the template, specifically by writing to `$this->template`. -Subsequently, the presenter returns the answer. This can be an HTML page, an image, an XML document, sending a file from disk, JSON or redirecting to another page. Importantly, if we do not explicitly say how to respond (which is the case of `ProductPresenter`), the answer will be to render the template with an HTML page. Why? Well, because in 99% of cases we want to draw a template, so the presenter takes this behavior as the default and wants to make our work easier. That's Nette's point. +Subsequently, the presenter returns a response. This could be an HTML page, an image, an XML document, sending a file from the disk, JSON, or perhaps a redirect to another page. Importantly, if we don't explicitly specify how to respond (which is the case with `ProductPresenter`), the response will be to render a template into an HTML page. Why? Because in 99% of cases, we want to render a template. Therefore, the presenter adopts this behavior as the default to simplify our work. That's the essence of Nette. -We don't even have to state which template to draw, he derives the path to it according to simple logic. In the case of presenter `Product` and action `show`, it tries to see if one of these template files exists relative to the directory where class `ProductPresenter` is located: +We don't even need to specify which template to render; the framework deduces the path automatically. In the case of the `show` action, it simply attempts to load the `show.latte` template located in the same directory as the `ProductPresenter` class. It also tries to find the layout in the `@layout.latte` file (more details on [template lookup |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` +Then, the templates are rendered. This completes the task of the presenter and the entire application. If the template doesn't exist, a 404 error page is returned. You can learn more about presenters on the [Presenters|presenters] page. -It will also try to find the layout in file `@layout.latte` and then it renders the template. Now the task of the presenter and the entire application is completed. If the template does not exist, a page with error 404 will be returned. You can read more about presenters on the [Presenters] page. +[* request-flow.svg *] -Just to be sure, let's try to recap the whole process with a slightly different URL: +To be sure, let's recap the entire process with a slightly different URL: -1) the URL will be `https://example.com` -2) we boot the application, create a container and run `Application::run()` -3) the router decodes the URL as a pair `Homepage:default` -4) an `HomepagePresenter` object is created -5) method `renderDefault()` is called (if exists) -6) a template `templates/Homepage/default.latte` with a layout `templates/@layout.latte` is rendered +1) The URL is `https://example.com` +2) The application boots, the DI container is created, and `Application::run()` is executed. +3) The router decodes the URL into the pair `Home:default`. +4) An instance of the `HomePresenter` class is created. +5) The `renderDefault()` method is called (if it exists). +6) The template, e.g., `default.latte`, is rendered along with the layout, e.g., `@layout.latte`. -You may have come across a lot of new concepts now, but we believe they make sense. Creating applications in Nette is a breeze. +You might have encountered many new concepts just now, but we believe they make sense. Developing applications in Nette is remarkably straightforward. Templates ========= -When it comes to the templates, Nette uses the [Latte |latte:] template system. That's why the files with templates ends with `.latte`. Latte is used because it is the most secure template system for PHP, and at the same time the most intuitive system. You don't have to learn much new, you just need to know PHP and a few Latte tags. You will find out everything [in the documentation |latte:]. +Speaking of templates, Nette uses the [Latte |latte:] templating system. That's why template files have the `.latte` extension. Latte is used primarily because it's the most secure templating system for PHP, and also the most intuitive. You don't need to learn much new; knowledge of PHP and a few tags is sufficient. You'll find everything you need [in the documentation |templates]. -In template we [create a links |creating-links] to other presenters & actions as follows: +In the template, you [create links |creating-links] to other presenters & actions like this: ```latte product detail ``` -Simply write the familiar `Presenter:action` pair instead of the real URL and include any parameters. The trick is `n:href`, which says that this attribute will be processed by Nette. And it will generate: +Simply write the familiar `Presenter:action` pair instead of the actual URL and include any necessary parameters. The trick lies in `n:href`, which tells Nette to process this attribute. It will then generate: ```latte product detail ``` -The previously mentioned router is in charge of generating the URL. In fact, routers in Nette are unique in that they can perform not only transformations from a URL to a pair of presenter:action, but also vice versa generate a URL from the name of the presenter + action + parameters. -Thanks to this, in Nette you can completely change the form of the URL in the whole finished application without changing a single character in the template or presenter just by modifying the router. -And thanks to this, the so-called canonization works, which is another unique feature of Nette, which improves SEO (optimization of searchability on the internet) by automatically preventing the existence of duplicate content at different URLs. -Many programmers find this amazing. +URL generation is handled by the aforementioned router. Routers in Nette are exceptional because they can perform not only the transformation from a URL to a `Presenter:action` pair but also the reverse: generating a URL from the presenter name, action, and parameters. Thanks to this, you can completely change the URL format throughout your entire finished application in Nette without altering a single character in the templates or presenters—simply by modifying the router. This also enables so-called canonization, another unique Nette feature that enhances SEO (Search Engine Optimization) by automatically preventing duplicate content from existing on different URLs. Many programmers find this capability astounding. Interactive Components ====================== -We have one more thing to tell you about presenters: they have a built-in component system. Older of you may remember something similar from Delphi or ASP.NET Web Forms. React or Vue.js is built on something remotely similar. In the world of PHP frameworks, this is a completely unique feature. +We need to tell you one more thing about presenters: they have a built-in component system. Those with more experience might recall something similar from Delphi or ASP.NET Web Forms; React or Vue.js are built on somewhat related concepts. In the world of PHP frameworks, this is a completely unique feature. -Components are separate reusable units that we place into pages (ie presenters). They can be [forms|forms:in-presenter], [datagrids |https://componette.org/contributte/datagrid/], menus, polls, in fact anything that makes sense to use repeatedly. We can create our own components or use some of the [huge range |https://componette.org] of opensource components. +Components are independent, reusable units that we embed into pages (i.e., presenters). These can be [forms |forms:in-presenter], [datagrids |https://componette.org/contributte/datagrid/], menus, polls—essentially anything that makes sense to reuse. We can create our own components or utilize some from the [vast selection |https://componette.org] of open-source components. -Components fundamentally change the approach to application development. They will open up new possibilities for composing pages from pre-defined units. And they have something in common with [Hollywood|components#Hollywood style]. +Components fundamentally influence the approach to application development. They open up new possibilities for composing pages from pre-prepared units. And they also have something in common with [Hollywood |components#Hollywood Style]. DI Container and Configuration ============================== -DI container (factory for objects) is the heart of the whole application. +The DI container, or object factory, is the heart of the entire application. -Don't worry, it's not a magical black box, as it might seem from the previous words. Actually, it's one pretty boring PHP class generated by Nette and stored in a cache directory. It has a lot of methods named as `createServiceAbcd()` and each of them creates and returns an object. Yes, there is also a method `createServiceApplication()` that will produce `Nette\Application\Application`, which we needed in the file `index.php` to run the application. And there are methods for producing individual presenters. And so on. +Don't worry, it's not some magical black box, as the preceding lines might suggest. In reality, it's a rather mundane PHP class generated by Nette and stored in the cache directory. It contains many methods named like `createServiceAbcd()`, each capable of creating and returning a specific object. Yes, there's also a `createServiceApplication()` method that produces the `Nette\Application\Application` instance we needed in `index.php` to run the application. There are also methods for creating individual presenters, and so on. -The objects that the DI container creates are called services for some reason. +The objects created by the DI container are, for some reason, called services. -What is really special about this class is that it is not programmed by you, but by the framework. It actually generates the PHP code and saves it to disk. You just give instructions on what objects the container should be able to produce and how exactly. And these instructions are written in [configuration files |bootstrap#DI Container Configuration] in the [NEON format|neon:format] and therefore have the extension `.neon`. +What's truly special about this class is that you don't program it—the framework does. It actually generates the PHP code and saves it to disk. You simply provide instructions on which objects the container should be able to create and how exactly. These instructions are written in [configuration files |bootstrapping#DI Container Configuration], which use the [NEON|neon:format] format and thus have the `.neon` extension. -The configuration files are used purely to instruct the DI container. So, for example, if I specify the `expiration: 14 days` option in the [session|http:configuration#Session] section, the DI container when creating the `Nette\Http\Session` object representing the session will call its method `setExpiration('14 days')`, and thus configuration becomes a reality. +Configuration files serve purely to instruct the DI container. So, for example, if you specify the `expiration: 14 days` option in the [session |http:configuration#Session] section, the DI container, when creating the `Nette\Http\Session` object representing the session, will call its `setExpiration('14 days')` method, thereby making the configuration a reality. -There is a whole chapter ready for you, describing what can be [configured |nette:configuring] and how to [define your own services |dependency-injection:services]. +There's an entire chapter prepared for you describing what can be [configured |nette:configuring] and how to [define your own services |dependency-injection:services]. -Once you get into the creation of services, you will come across the word [autowiring |dependency-injection:autowiring]. This is a gadget that will make your life incredibly easier. It can automatically pass objects where you need them (in the constructors of your classes, for example) without having to do anything. You will find that the DI container in Nette is a small miracle. +Once you delve a bit into service creation, you'll encounter the term [autowiring |dependency-injection:autowiring]. This is a feature that will simplify your life incredibly. It can automatically pass objects where you need them (for example, in the constructors of your classes) without you having to do anything. You'll discover that the DI container in Nette is a small miracle. What Next? ========== -We went through the basic principles of applications in Nette. So far, very superficially, but you will soon delve into the depths and eventually create wonderful web applications. Where to continue? Have you tried the tutorial [Create Your First Application |quickstart:getting-started]? +We've covered the fundamental principles of Nette applications. While it's been a surface-level overview so far, you'll soon delve deeper and, in time, create amazing web applications. Where to go next? Have you tried the [Create Your First Application|quickstart:] tutorial yet? -In addition to the above, Nette has a whole arsenal of [useful classes |utils:], [database layer |database:], etc. Try purposely just click through documentation. Or visit [blog |https://blog.nette.org]. You will discover a lot of interesting things. +In addition to what's described above, Nette offers a whole arsenal of [useful classes|utils:], a [database layer|database:], etc. Try clicking through the documentation. Or visit the [blog|https://blog.nette.org]. You'll discover many interesting things. -Let the framework bring you a lot of joy 💙 +May the framework bring you much joy 💙 diff --git a/application/en/modules.texy b/application/en/modules.texy deleted file mode 100644 index ba6ba2675f..0000000000 --- a/application/en/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Modules -******* - -.[perex] -In Nette, modules represent the logical units that make up an application. They include presenters, templates, possibly also components and model classes. - -One directory for presenters and one for templates would not be enough for real projects. Having dozens of files in one folder is at least unorganized. How to get out of it? We simply split them into subdirectories on disk and into namespaces in the code. And that's exactly what the Nette modules do. - -So let's forget about a single folder for presenters and templates and instead create modules, for example `Admin` and `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← directory with modules -│ ├── Admin/ ← module Admin -│ │ ├── Presenters/ ← its presenters -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← module Front -│ └── Presenters/ ← its presenters -│ └── ... -\-- - -This directory structure will be reflected by the class namespaces, so for example `DashboardPresenter` will be in the `App\Modules\Admin\Presenters` namespace: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -The `Dashboard` presenter inside the `Admin` module is referenced within the application using the colon notation as `Admin:Dashboard`, and its `default` action as `Admin:Dashboard:default`. -And how does Nette proper know that `Admin:Dashboard` represents the `App\Modules\Admin\Presenters\DashboardPresenter` class? This is determined by [#mapping] in the configuration. -Thus, the given structure is not hard set and you can modify it according to your needs. - -Modules can of course contain all other items besides presenters and templates, such as components, model classes, etc. - - -Nested Modules --------------- - -Modules don't have to form only a flat structure, you can also create submodules, for example: - -/--pre -app/ -├── Modules/ ← directory with modules -│ ├── Blog/ ← module Blog -│ │ ├── Admin/ ← submodule Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodule Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← module Forum -│ │ └── ... -\-- - -Thus, the `Blog` module is divided into `Admin` and `Front` submodules. Again, this will be reflected in the namespaces, which will be `App\Modules\Blog\Admin\Presenters` etc. The presenter `Dashboard` inside the submodule is referred to as `Blog:Admin:Dashboard`. - -The nesting can go as deep as you like, so sub-submodules can be created. - - -Creating Links --------------- - -Links in presenter templates are relative to the current module. Thus, the link `Foo:default` leads to the presenter `Foo` in the same module as the current presenter. If the current module is `Front`, for example, then the link goes like this: - -```latte -link to Front:Product:show -``` - -A link is relative even if it includes the name of a module, which is then considered a submodule: - -```latte -link to Front:Shop:Product:show -``` - -Absolute links are written analogously to absolute paths on disk, but with colons instead of slashes. Thus, an absolute link starts with a colon: - -```latte -link to Admin:Product:show -``` - -To find out if we are in a certain module or its submodule we can use `isModuleCurrent(moduleName)` function. - -```latte -
  • - ... -
  • -``` - - -Routing -------- - -See [chapter on routing |routing#Modules]. - - -Mapping -------- - -Defines the rules by which the class name is derived from the presenter name. We write them in [configuration] under the `application › mapping` key. - -Let's start with a sample that doesn't use modules. We'll just want the presenter classes to have the `App\Presenters` namespace. That means that a presenter such as `Homepage` should map to the `App\Presenters\HomepagePresenter` class. This can be achieved by the following configuration: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -The presenter name is replaced with the asterisk in the class mask and the result is the class name. Easy! - -If we divide presenters into modules, we can have our own mapping for each module: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Now presenter `Front:Homepage` maps to class ``App\Modules\Front\Presenters\HomepagePresenter` and presenter `Admin:Dashboard` to class `App\Modules\Admin\Presenters\DashboardPresenter`. - -It is more practical to create a general (star) rule to replace the first two. The extra asterisk will be added to the class mask just for the module: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -But what if we use nested modules and have a presenter `Admin:User:Edit`? In this case, the segment with an asterisk representing the module for each level is simply repeated and the result is class `App\Modules\Admin\User\Presenters\EditPresenter`. - -An alternative notation is to use an array consisting of three segments instead of a string. This notation is equivalent to the previous one: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -The default value is `*: *Module\*Presenter`. diff --git a/application/en/multiplier.texy b/application/en/multiplier.texy index 2ca1b92466..2fc8d760c4 100644 --- a/application/en/multiplier.texy +++ b/application/en/multiplier.texy @@ -1,15 +1,17 @@ Multiplier: Dynamic Components ****************************** -A tool for dynamical creation of interactive components .[perex] +.[perex] +A tool for dynamic creation of interactive components. -Let's start with a typical problem: we have a list of products on an e-commerce site and we want to accompany each product with an *add to cart* form. One way is to wrap the whole listing in a single form. A more convenient way is to use [api:Nette\Application\UI\Multiplier]. +Let's start with a typical example: imagine a product list in an e-shop where you want an 'Add to Cart' form for each item. One possible approach is to wrap the entire listing in a single form. However, a much more convenient method is offered by [api:Nette\Application\UI\Multiplier]. -Multiplier allows you to define a factory for multiple components. It is based on the principle of nested components - each component inheriting from [api:Nette\ComponentModel\Container] may contain other components. +Multiplier allows you to conveniently define a factory for multiple components. It works on the principle of nested components – any component inheriting from [api:Nette\ComponentModel\Container] can contain other components. -See [component model|components#Components in Depth] in the documentation. .[tip] +.[tip] +See the chapter on the [component model |components#Components in Depth] in the documentation. -Multiplier poses as a parent component which can dynamically create its children using the callback passed in the constructor. See example: +The essence of Multiplier is that it acts as a parent that can dynamically create its children using a callback passed in the constructor. See the example: ```php protected function createComponentShopForm(): Multiplier @@ -24,7 +26,7 @@ protected function createComponentShopForm(): Multiplier } ``` -In the template we can render a form for each product - and each form will indeed be a unique component. +Now, in the template, we can simply render the form for each product – and each one will truly be a unique component. ```latte {foreach $items as $item} @@ -35,16 +37,16 @@ In the template we can render a form for each product - and each form will indee {/foreach} ``` -Argument passed to `{control}` tag says: +The argument passed in the `{control}` tag follows a format that means: -1. get a component `shopForm` -2. and return its child `$item->id` +1. Get the component `shopForm`. +2. From it, get the child named `$item->id`. -During the first call of **1.** the `shopForm` component does not yet exist, so the method `createComponentShopForm` is called to create it. An anonymous function passed as a parameter to Multiplier, is then called and a form is created. +During the first call of point **1**, the `shopForm` component doesn't exist yet, so its factory `createComponentShopForm` is called. Then, on the obtained component (an instance of Multiplier), the factory for the specific form is called – which is the anonymous function we passed to the Multiplier's constructor. -In the subsequent iterations of the `foreach` the method `createComponentShopForm` is no longer called because the component already exists. But since we reference another child (`$item->id` varies between iterations), an anonymous function is called again and a new form is created. +In the next iteration of the foreach loop, the `createComponentShopForm` method will not be called again (as the component already exists). However, because we are looking for a different child (since `$item->id` will be different in each iteration), the anonymous function will be called again, returning a new form. -The last thing is to ensure that the form actually adds the correct product to the cart because in the current state all the forms are equal and we cannot distinguish to which products they belong. For this we can use the property of Multiplier (and in general of any component factory method in Nette Framework) that every component factory method receives the name of the created component as the first argument. In our case that would be `$item->id`, which is exactly what we need to distinguish individual products. All you need to do is modify the code for creating the form: +The only thing left is to ensure that the form adds the correct product to the cart – currently, the form is identical for every product. A feature of Multiplier (and generally of any component factory in Nette Framework) helps us here: every factory receives the name of the component being created as its first argument. In our case, this will be `$item->id`, which is precisely the information we need. So, we just need to slightly modify the form creation: ```php protected function createComponentShopForm(): Multiplier diff --git a/application/en/presenters.texy b/application/en/presenters.texy index 542369cdfc..3d36da3b2c 100644 --- a/application/en/presenters.texy +++ b/application/en/presenters.texy @@ -3,48 +3,45 @@ Presenters
    -We will learn how to write presenters and templates in Nette. After reading you will know: +We will explore how presenters and templates are written in Nette. After reading, you will understand: -- how the presenter works -- what are persistent parameters -- how to render a template +- how presenters work +- what persistent parameters are +- how templates are rendered
    -[We already know |how-it-works#nette-application] that a presenter is a class that represents a specific page of a web application, such as a homepage; product in e-shop; sign-in form; sitemap feed, etc. The application can have from one to thousands of presenters. In other frameworks, they are also known as controllers. +[We already know |how-it-works#Nette Application] that a presenter is a class representing a specific page of a web application, such as the homepage, a product in an e-shop, a login form, a sitemap feed, etc. An application can have anywhere from one to thousands of presenters. In other frameworks, they are also known as controllers. -Usually, the term presenter refers to a descendant of the class [api:Nette\Application\UI\Presenter], which is suitable for web interfaces and which we will discuss in the rest of this chapter. In a general sense, a presenter is any object that implements the [api:Nette\Application\IPresenter] interface. +Usually, the term presenter refers to a descendant of the [api:Nette\Application\UI\Presenter] class, which is suitable for generating web interfaces and will be the focus of the rest of this chapter. In a general sense, a presenter is any object implementing the [api:Nette\Application\IPresenter] interface. -Life Cycle of Presenter -======================= +Presenter Life Cycle +==================== -The job of the presenter is to process the request and return a response (which can be an HTML page, image, redirect, etc.). +The presenter's task is to handle a request and return a response (which could be an HTML page, an image, a redirect, etc.). -So at the beginning is a request. It is not directly an HTTP request, but an [api:Nette\Application\Request] object into which the HTTP request was transformed using a router. We usually do not come into contact with this object, because the presenter cleverly delegates the processing of the request to special methods, which we will now see. +So, initially, a request is passed to it. This isn't the direct HTTP request, but a [api:Nette\Application\Request] object, into which the HTTP request was transformed with the help of the router. We usually don't interact directly with this object, as the presenter cleverly delegates request processing to other methods, which we will now explore. -[* lifecycle.svg *] *** *Life cycle of presenter* .<> +[* lifecycle.svg *] *** Presenter Life Cycle .<> -The figure shows a list of methods that are called sequentially from top to bottom, if they exist. None of them need to exist, we can have a completely empty presenter without a single method and build a simple static web on it. +The diagram shows a list of methods that are called sequentially from top to bottom, if they exist. None of them are mandatory; you can have a completely empty presenter without a single method and build a simple static website upon it. `__construct()` --------------- -The constructor does not belong exactly to the life cycle of the presenter, because it is called at the moment of creating the object. But we mention it because of its importance. The constructor (together with [method inject|best-practices:inject-method-attribute]) is used to pass dependencies. +The constructor doesn't strictly belong to the presenter's life cycle, as it's called at the moment the object is created. However, we mention it due to its importance. The constructor (along with the [inject method|best-practices:inject-method-attribute]) is used for passing dependencies. -The presenter should not take care of the business logic of the application, write and read from the database, perform calculations, etc. This is the task for classes from a layer, which we call a model. For example, class `ArticleRepository` may be responsible for loading and saving articles. In order for the presenter to use it, it is [passed using dependency injection |dependency-injection:passing-dependencies]: +The presenter should not handle the application's business logic, write to or read from the database, perform calculations, etc. That's the responsibility of classes in the layer we call the model. For example, an `ArticleRepository` class might be responsible for loading and saving articles. For the presenter to work with it, it needs to have it [passed via dependency injection |dependency-injection:passing-dependencies]: ```php class ArticlePresenter extends Nette\Application\UI\Presenter { - /** @var ArticleRepository */ - private $articles; - - public function __construct(ArticleRepository $articles) - { - $this->articles = $articles; + public function __construct( + private ArticleRepository $articles, + ) { } } ``` @@ -53,44 +50,44 @@ class ArticlePresenter extends Nette\Application\UI\Presenter `startup()` ----------- -Immediately after receiving the request, method `startup ()` is invoked. You can use it to initialize properties, check user privileges, etc. It is required to always call the `parent::startup()` ancestor. +Immediately after receiving the request, the `startup()` method is invoked. You can use it to initialize properties, check user permissions, etc. It is required that this method always calls its parent: `parent::startup()`. `action(args...)` .{toc: action()} -------------------------------------------------- -Similar to the method `render()`. While `render()` is intended to prepare data for a specific template, which is subsequently rendered, in `action()` a request is processed without following-up template rendering. For example, data is processed, a user is logged in or out, and so on, and then it [redirects elsewhere |#Redirection]. +Similar to the `render()` method. While `render()` is intended to prepare data for a specific template that will subsequently be rendered, `action()` processes a request without necessarily rendering a template afterwards. For example, it might process data, log a user in or out, and so on, and then [redirect elsewhere |#Redirection]. -It is important that `action()` is called before `render()`, so inside it we can possibly change the next course of life cycle, i.e. change the template that will be rendered and also the method `render()` that will be called, using `setView('otherView')`. +It's important that `action()` is called *before* `render()`. This allows us to potentially change the course of the request within the action method, for instance, by changing the template that will be rendered or even the `render()` method that will be called, using `setView('otherView')`. -The parameters from the request are passed to the method. It is possible and recommended to specify types for the parameters, e.g. `actionShow(int $id, string $slug = null)` - if parameter `id` is missing or if it is not an integer, the presenter returns [error 404|#Error 404 etc.] and terminates the operation. +Parameters from the request are passed to the method. It's possible and recommended to specify types for these parameters, e.g., `actionShow(int $id, ?string $slug = null)`. If the `id` parameter is missing or is not an integer, the presenter returns a [404 error |#Error 404 etc] and terminates. `handle(args...)` .{toc: handle()} -------------------------------------------------- -This method processes the so-called signals, which we will discuss in the chapter about [Components |components#Signal]. It is intended mainly for components and processing of AJAX requests. +This method processes so-called signals, which we'll learn about in the chapter dedicated to [components |components#Signal]. It's primarily intended for components and handling AJAX requests. -The parameters are passed to the method, as in the case of `action()`, including type checking. +Parameters from the request are passed to the method, just like with `action()`, including type checking. `beforeRender()` ---------------- -Method `beforeRender`, as the name suggests, is called before each method `render()`. Is used for common template configuration, passing variables for layout and so on. +The `beforeRender` method, as its name suggests, is called before every `render()` method. It's used for common template configuration, passing variables to the layout, and similar tasks. `render(args...)` .{toc: render()} ---------------------------------------------- -The place where we prepare the template for subsequent rendering, we pass data to it, etc. +This is where we prepare the template for subsequent rendering, pass data to it, etc. -The parameters are passed to the method, as in the case of `action()`, including type checking. +Parameters from the request are passed to the method, just like with `action()`, including type checking. ```php public function renderShow(int $id): void { - // we obtain data from the model and pass it to the template + // obtain data from the model and pass it to the template $this->template->article = $this->articles->getById($id); } ``` @@ -99,39 +96,39 @@ public function renderShow(int $id): void `afterRender()` --------------- -Method `afterRender`, as the name suggests again, is called after each `render()` method. It is used rather rarely. +The `afterRender` method, as the name suggests again, is called after every `render()` method. It's used rather rarely. `shutdown()` ------------ -It is called at the end of the presenter's life cycle. +Called at the end of the presenter's life cycle. -**Good advice before we move on**. As you can see, the presenter can handle more actions/views, i.e. have more methods `render()`. But we recommend designing presenters with one or as few actions as possible. +**A piece of advice before we continue:** As you can see, a presenter can handle multiple actions/views, meaning it can have multiple `render()` methods. However, we recommend designing presenters with one or as few actions as possible. Sending a Response ================== -The presenter's response is usually [rendering the template with the HTML page|templates], but it can also be sending a file, JSON or even redirecting to another page. +The presenter's response is typically [rendering a template into an HTML page|templates], but it can also be sending a file, JSON, or even redirecting to another page. -At any time during the lifecycle, you can use any of the following methods to send a response and exit the presenter at the same time: +At any point during the life cycle, we can use one of the following methods to send a response and simultaneously terminate the presenter: -- `redirect()`, `redirectPermanent()`, `redirectUrl()` and `forward()` [redirects |#Redirection] -- `error()` quits presenter [due to error |#Error 404 etc.] -- `sendJson($data)` quits presenter and [sends the data |#Sending JSON] in JSON format -- `sendTemplate()` quits presenter and immediately [renderes the template |templates] -- `sendResponse($response)` quits presenter and sends [own response |#Responses] -- `terminate()` quits presenter without answer +- `redirect()`, `redirectPermanent()`, `redirectUrl()`, and `forward()` perform a [redirect |#Redirection] +- `error()` terminates the presenter [due to an error |#Error 404 etc] +- `sendJson($data)` terminates the presenter and [sends data |#Sending JSON] in JSON format +- `sendTemplate()` terminates the presenter and immediately [renders the template |templates] +- `sendResponse($response)` terminates the presenter and sends a [custom response |#Responses] +- `terminate()` terminates the presenter without a response -If you do not call any of these methods, the presenter will automatically proceed to render the template. Why? Well, because in 99% of cases we want to draw a template, so the presenter takes this behavior as the default and wants to make our work easier. +If you don't call any of these methods, the presenter automatically proceeds to render the template. Why? Because in 99% of cases, we want to render a template, so the presenter adopts this behavior as the default to simplify our work. Creating Links ============== -Presenter has a method `link()`, which is used to create URL links to other presenters. The first parameter is the target presenter & action, followed by the arguments, which can be passed as array: +The presenter has a `link()` method used to create URL links to other presenters. The first parameter is the target presenter & action, followed by arguments, which can be passed as an array: ```php $url = $this->link('Product:show', $id); @@ -139,64 +136,64 @@ $url = $this->link('Product:show', $id); $url = $this->link('Product:show', [$id, 'lang' => 'en']); ``` -In template we create links to other presenters & actions as follows: +In the template, links to other presenters & actions are created like this: ```latte product detail ``` -Simply write the familiar `Presenter:action` pair instead of the real URL and include any parameters. The trick is `n:href`, which says that this attribute will be processed by Latte and generates a real URL. In Nette, you don't have to think about URLs at all, just about presenters and actions. +Simply write the familiar `Presenter:action` pair instead of the actual URL and include any necessary parameters. The trick lies in `n:href`, which tells Latte to process this attribute and generate the real URL. In Nette, you don't need to think about URLs at all, just about presenters and actions. -For more information, see [Creating Links]. +You can find more information in the chapter [Creating URL Links|creating-links]. Redirection =========== -Methods `redirect()` and `forward()` are used to jump to another presenter, which have a very similar syntax as the method [link() |#Creating Links]. +The `redirect()` and `forward()` methods are used to switch to another presenter. They have a very similar syntax to the [link() |#Creating Links] method. -The `forward()` switches to the new presenter immediately without HTTP redirection: +The `forward()` method switches to the new presenter immediately without an HTTP redirect: ```php $this->forward('Product:show'); ``` -Example of temporary redirection with HTTP code 302 or 303: +Example of a temporary redirect with HTTP code 302 (or 303 if the current request method is POST): ```php $this->redirect('Product:show', $id); ``` -To achieve permanent redirection with HTTP code 301 use: +To achieve a permanent redirect with HTTP code 301, use this: ```php $this->redirectPermanent('Product:show', $id); ``` -You can redirect to another URL outside the application with the `redirectUrl()` method: +You can redirect to another URL outside the application using the `redirectUrl()` method. The HTTP code can be specified as the second parameter; the default is 302 (or 303 if the current request method is POST): ```php $this->redirectUrl('https://nette.org'); ``` -Redirection immediately terminates the presenter's life cycle by throwing the so-called silent termination exception `Nette\Application\AbortException`. +Redirection immediately terminates the presenter's activity by throwing the so-called silent termination exception, `Nette\Application\AbortException`. -Before redirection, it is possible to send a [flash message |#Flash Messages], messages that will be displayed in the template after redirection. +Before redirection, it's possible to send [#flash messages], i.e., messages that will be displayed in the template after redirection. Flash Messages ============== -These are messages that usually inform about the result of an operation. An important feature of flash messages is that they are available in the template even after redirection. Even after being displayed, they will remain alive for another 30 seconds - for example, in case the user would unintentionally refresh the page - the message will not be lost. +These are messages typically informing about the result of some operation. An important feature of flash messages is that they remain available in the template even after redirection. Once displayed, they stay active for an additional 30 seconds – for instance, if the user refreshes the page due to a transmission error, the message won't disappear immediately. -Just call the [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] method and presenter will take care of passing the message to the template. The first argument is the text of the message and the second optional argument is its type (error, warning, info etc.). The method `flashMessage()` returns an instance of flash message, to allow us to add more information. +Simply call the [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] method, and the presenter handles passing it to the template. The first parameter is the message text, and the optional second parameter is its type (e.g., error, warning, info). The `flashMessage()` method returns an instance of the flash message, allowing additional information to be added. ```php -$this->flashMessage('Item was removed.'); -$this->redirect(/* ... */); +$this->flashMessage('The item has been deleted.'); +$this->redirect(/* ... */); // and redirect ``` -In the template, these messages are available in the variable `$flashes` as objects `stdClass`, which contain the properties `message` (message text), `type` (message type) and can contain the already mentioned user information. We draw them as follows: +In the template, these messages are available in the `$flashes` variable as `stdClass` objects containing the properties `message` (the message text), `type` (the message type), and potentially the user-added information mentioned earlier. We render them like this: ```latte {foreach $flashes as $flash} @@ -208,7 +205,7 @@ In the template, these messages are available in the variable `$flashes` as obje Error 404 etc. ============== -When we can't fulfill the request because for example the article we want to display does not exist in the database, we will throw out the 404 error using method `error(string $message = null, int $httpCode = 404)`, which represents HTTP error 404: +If the request cannot be fulfilled, for example, because the article we want to display doesn't exist in the database, we throw a 404 error using the `error(?string $message = null, int $httpCode = 404)` method. ```php public function renderShow(int $id): void @@ -221,14 +218,13 @@ public function renderShow(int $id): void } ``` -The HTTP error code can be passed as the second parameter, the default is 404. The method works by throwing exception `Nette\Application\BadRequestException`, after which `Application` passes control to the error-presenter. Which is a presenter whose job is to display a page informing about the error. -The error-preseter is set in [application configuration |configuration]. +The HTTP error code can be passed as the second parameter; the default is 404. The method works by throwing a `Nette\Application\BadRequestException`, after which the `Application` passes control to the error presenter. This is a presenter whose task is to display a page informing about the error that occurred. The error presenter is configured in the [application configuration|configuration]. Sending JSON ============ -Example of action-method that sends data in JSON format and exits the presenter: +Example of an action method that sends data in JSON format and terminates the presenter: ```php public function actionData(): void @@ -239,100 +235,155 @@ public function actionData(): void ``` -Persistent Parameters -===================== +Request Parameters .{data-version:3.1.14} +========================================= -Persistent parameters are **transferred automatically** in links. This means that we do not have to explicitly specify them in every `link()` or `n:href` in the template, but they will still be transferred. +The presenter, and also each component, obtains its parameters from the HTTP request. You can retrieve their values using the `getParameter($name)` or `getParameters()` methods. The values are strings or arrays of strings, essentially raw data obtained directly from the URL. -If your application has multiple language versions, then the current language is a parameter that must always be part of the URL. And it would be incredibly tiring to mention it in every link. That's not necessary with Nette. We simply mark the `lang` parameter as persistent in this way: +For greater convenience, we recommend accessing parameters via properties. Simply mark them with the `#[Parameter]` attribute: ```php -class ProductPresenter extends Nette\Application\UI\Presenter +use Nette\Application\Attributes\Parameter; // this line is important + +class HomePresenter extends Nette\Application\UI\Presenter { - /** @persistent */ - public $lang; + #[Parameter] + public string $theme; // must be public } ``` -If the current value of the parameter `lang` is `'en'`, then the URL created with `link()` or `n:href` in the template will contain `lang=en`. Great! +For the property, we recommend specifying the data type (e.g., `string`), and Nette will automatically cast the value accordingly. Parameter values can also be [validated |#Validation of Parameters]. -However, we can also add parameter `lang` and by that change its value: +When creating a link, you can set the parameter's value directly: ```latte -detail in English +click ``` -Or, conversely, it can be removed by setting to null: -```latte -click here -``` +Persistent Parameters +===================== -The persistent variable must be declared as public. We can also specify a default value. If the parameter has the same value as the default, it will not be included in the URL. +Persistent parameters are used to maintain state across different requests. Their value remains the same even after clicking a link. Unlike session data, they are transmitted in the URL. And this happens completely automatically, so there's no need to explicitly include them in `link()` or `n:href`. -Persistence reflects the hierarchy of presenter classes, thus parameter defined in a certain presenter or trait is then automatically transferred to each presenter inheriting from it or using the same trait. +An example use case? Imagine you have a multilingual application. The current language is a parameter that must always be part of the URL. But it would be incredibly tedious to include it in every link. So, you make it a persistent parameter `lang`, and it will be carried along automatically. Neat! -In PHP 8, you can also use attributes to mark persistent parameters: +Creating a persistent parameter in Nette is extremely simple. Just create a public property and mark it with the attribute: (previously, `/** @persistent */` was used) ```php -use Nette\Application\Attributes\Persistent; +use Nette\Application\Attributes\Persistent; // this line is important class ProductPresenter extends Nette\Application\UI\Presenter { #[Persistent] - public $lang; + public string $lang; // must be public +} +``` + +If `$this->lang` has a value like `'en'`, then links created using `link()` or `n:href` will also contain the parameter `lang=en`. And after clicking the link, `$this->lang` will again be `'en'`. + +For the property, we recommend specifying the data type (e.g., `string`), and you can also provide a default value. Parameter values can be [validated |#Validation of Parameters]. + +Persistent parameters are typically transferred between all actions of a given presenter. To transfer them across multiple presenters as well, they need to be defined either: + +- in a common ancestor from which the presenters inherit +- or in a trait that the presenters use: + +```php +trait LanguageAware +{ + #[Persistent] + public string $lang; +} + +class ProductPresenter extends Nette\Application\UI\Presenter +{ + use LanguageAware; } ``` +When creating a link, the value of a persistent parameter can be changed: + +```latte +detail in Czech +``` + +Alternatively, it can be *reset*, i.e., removed from the URL. It will then assume its default value: + +```latte +click +``` + Interactive Components ====================== -Presenters have a built-in component system. Components are separate reusable units that we place into presenters. They can be [forms|forms:in-presenter], datagrids, menus, in fact anything that makes sense to use repeatedly. +Presenters have a built-in component system. Components are separate reusable units that we embed into presenters. They can be [forms |forms:in-presenter], datagrids, menus, essentially anything that makes sense to use repeatedly. -How are components placed and subsequently used in the presenter? This is explained in chapter [Components]. You'll even find out what they have to do with Hollywood. +How are components embedded into presenters and subsequently used? You'll learn this in the [Components |components] chapter. You'll even find out what they have in common with Hollywood. -Where Can I Get Some Components? On page [Componette |https://componette.org] you can find some open-source components and other addons for Nette that are made and shared by the community of Nette Framework. +And where can I get components? On [Componette |https://componette.org/search/component], you'll find open-source components and many other add-ons for Nette, contributed by volunteers from the framework community. Going Deeper ============ .[tip] -What we have shown so far in this chapter will probably suffice. The following lines are intended for those who are interested in presenters in depth and want to know everything. +What we've covered so far in this chapter will likely be sufficient for most uses. The following sections are intended for those interested in delving deeper into presenters and wanting to know absolutely everything. + + +Validation of Parameters +------------------------ +The values of [#Request-Parameters] and [#Persistent-Parameters] received from URLs are written to properties by the `loadState()` method. It also checks if the data type specified in the property matches, otherwise it will respond with a 404 error and the page will not be displayed. -Requirement and Parameters --------------------------- +Never blindly trust parameters received from the URL, as they can easily be overwritten by the user. For example, this is how we would verify if the language `$this->lang` is among the supported ones. A suitable way to do this is by overriding the aforementioned `loadState()` method: -The request handled by the presenter is the [api:Nette\Application\Request] object and is returned by the presenter's method `getRequest()`. It includes an array of parameters and each of them belongs either to some of the components or directly to the presenter (which is actually also a component, albeit a special one). So Nette redistributes the parameters and passes between the individual components (and the presenter) by calling the method `loadState(array $params)`, which is further described in the chapter [Components]. The parameters can be obtained by the method `getParameters(): array`, individually using `getParameter($name)`. Parameter values ​​are strings or arrays of strings, they are basically raw data obtained directly from a URL. +```php +class ProductPresenter extends Nette\Application\UI\Presenter +{ + #[Persistent] + public string $lang; + + public function loadState(array $params): void + { + parent::loadState($params); // $this->lang is set here + // followed by custom value check: + if (!in_array($this->lang, ['en', 'cs'])) { + $this->error(); + } + } +} +``` Save and Restore the Request ---------------------------- -You can save the current request to a session or restore it from the session and let the presenter execute it again. This is useful, for example, when a user fills out a form and its login expires. In order not to lose data, before redirecting to the sign-in page, we save the current request to the session using `$reqId = $this->storeRequest()`, which returns an identifier in the form of a short string and passes it as a parameter to the sign-in presenter. +The request handled by the presenter is a [api:Nette\Application\Request] object, returned by the presenter's `getRequest()` method. -After sign in, we call the method `$this->restoreRequest($reqId)`, which picks up the request from the session and forwards it to it. The method verifies that the request was created by the same user as now logged in is. If another user logs in or the key is invalid, it does nothing and the program continues. +The current request can be saved to the session or, conversely, restored from it and have the presenter execute it again. This is useful, for example, when a user is filling out a form and their login session expires. To avoid data loss, before redirecting to the login page, we save the current request to the session using `$reqId = $this->storeRequest()`. This returns its identifier as a short string, which we then pass as a parameter to the login presenter. -See the cookbook [How to return to an earlier page |best-practices:restore-request]. +After logging in, we call the `$this->restoreRequest($reqId)` method, which retrieves the request from the session and forwards to it. The method verifies that the request was created by the same user who is now logged in. If a different user logs in or the key is invalid, it does nothing, and the program continues as usual. + +See the guide [How to Return to a Previous Page |best-practices:restore-request]. Canonization ------------ -Presenters have one really great feature that improves SEO (optimization of searchability on the Internet). They automatically prevent the existence of duplicate content at different URLs. If multiple URLs lead to a certain destination, e.g. `/index` and `/index?page=1`, the framework designates one of them as the primary (canonical) and redirects the others to it using HTTP code 301. Thanks to this, search engines do not index pages twice and do not weaken their page rank. +Presenters have a truly excellent feature that contributes to better SEO (Search Engine Optimization). They automatically prevent the existence of duplicate content at different URLs. If multiple URLs lead to a specific destination, e.g., `/index` and `/index?page=1`, the framework designates one of them as primary (canonical) and redirects the others to it using HTTP code 301. Thanks to this, search engines don't index your pages twice and dilute their page rank. -This process is called canonization. The canonical URL is the URL generated by [router |routing], usually the first appropriate route in the collection. +This process is called canonization. The canonical URL is the one generated by the [router|routing], typically the first matching route in the collection. -Canonization is on by default and can be turned off via `$this->autoCanonicalize = false`. +Canonization is enabled by default and can be disabled via `$this->autoCanonicalize = false`. -Redirection does not occur with an AJAX or POST request because it would result in data loss or no SEO added value. +Redirection does not occur during AJAX or POST requests, as this could lead to data loss or would offer no added SEO value. -You can also invoke canonization manually using method `canonicalize()`, which, like method `link()`, receives the presenter, actions, and parameters as arguments. It creates a link and compares it to the current URL. If it is different, it redirects to the generated link. +You can also trigger canonization manually using the `canonicalize()` method. Similar to the `link()` method, you pass it the presenter, action, and parameters. It generates a link and compares it with the current URL address. If they differ, it redirects to the generated link. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // redirects if $slug is different from $realSlug @@ -344,7 +395,7 @@ public function actionShow(int $id, string $slug = null): void Events ------ -In addition to methods `startup()`, `beforeRender()` and `shutdown()`, which are called as part of the presenter's life cycle, other functions can be defined to be called automatically. The presenter defines the so-called [events |nette:glossary#events], and you add their handlers to arrays `$onStartup`, `$onRender` and `$onShutdown`. +In addition to the `startup()`, `beforeRender()`, and `shutdown()` methods, which are called as part of the presenter's life cycle, other functions can be defined to be called automatically. The presenter defines so-called [events |nette:glossary#Events], and you add their handlers to the `$onStartup`, `$onRender`, and `$onShutdown` arrays. ```php class ArticlePresenter extends Nette\Application\UI\Presenter @@ -358,23 +409,23 @@ class ArticlePresenter extends Nette\Application\UI\Presenter } ``` -Handlers in array `$onStartup` are called just before the method `startup()`, then `$onRender` between `beforeRender()` and `render()` and finally `$onShutdown` just before `shutdown()`. +Handlers in the `$onStartup` array are called just before the `startup()` method, `$onRender` handlers between `beforeRender()` and `render()`, and finally `$onShutdown` handlers just before `shutdown()`. Responses --------- -The response returned by the presenter is an object implementing the [api:Nette\Application\Response] interface. There are a number of ready-made answers: +The response returned by the presenter is an object implementing the [api:Nette\Application\Response] interface. Several pre-built responses are available: - [api:Nette\Application\Responses\CallbackResponse] - sends a callback - [api:Nette\Application\Responses\FileResponse] - sends the file -- [api:Nette\Application\Responses\ForwardResponse] - forward () +- [api:Nette\Application\Responses\ForwardResponse] - forward() - [api:Nette\Application\Responses\JsonResponse] - sends JSON - [api:Nette\Application\Responses\RedirectResponse] - redirect - [api:Nette\Application\Responses\TextResponse] - sends text - [api:Nette\Application\Responses\VoidResponse] - blank response -Responses are sent by method `sendResponse()`: +Responses are sent using the `sendResponse()` method: ```php use Nette\Application\Responses; @@ -393,3 +444,57 @@ $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $ht }; $this->sendResponse(new Responses\CallbackResponse($callback)); ``` + + +Access Restriction Using `#[Requires]` .{data-version:3.2.2} +------------------------------------------------------------ + +The `#[Requires]` attribute provides advanced options for restricting access to presenters and their methods. It can be used to specify HTTP methods, require an AJAX request, restrict to the same origin, and allow access only via forwarding. The attribute can be applied both to presenter classes and to individual methods like `action()`, `render()`, `handle()`, and `createComponent()`. + +You can specify these restrictions: +- on HTTP methods: `#[Requires(methods: ['GET', 'POST'])]` +- requiring an AJAX request: `#[Requires(ajax: true)]` +- access only from the same origin: `#[Requires(sameOrigin: true)]` +- access only via forwarding: `#[Requires(forward: true)]` +- restrictions on specific actions: `#[Requires(actions: 'default')]` + +Details can be found in the guide [How to Use the Requires Attribute |best-practices:attribute-requires]. + + +HTTP Method Check +----------------- + +Presenters in Nette automatically verify the HTTP method of every incoming request, primarily for security reasons. By default, the methods `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` are allowed. + +If you want to additionally allow, for example, the `OPTIONS` method, use the `#[Requires]` attribute (since Nette Application v3.2): + +```php +#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] +class MyPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +In version 3.1, verification is performed in `checkHttpMethod()`, which checks if the method specified in the request is included in the `$presenter->allowedMethods` array. Add a method like this: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +It's important to emphasize that if you enable the `OPTIONS` method, you must subsequently handle it appropriately within your presenter. This method is often used as a so-called preflight request, which the browser automatically sends before the actual request when it's necessary to determine if the request is permissible according to the CORS (Cross-Origin Resource Sharing) policy. If you enable the method but don't implement the correct response, it can lead to inconsistencies and potential security problems. + + +Further Reading +=============== + +- [Inject methods and attributes |best-practices:inject-method-attribute] +- [Composing presenters from traits |best-practices:presenter-traits] +- [Passing settings to presenters |best-practices:passing-settings-to-presenters] +- [How to Return to a Previous Page |best-practices:restore-request] diff --git a/application/en/routing.texy b/application/en/routing.texy index eedae4629a..daa1c181d8 100644 --- a/application/en/routing.texy +++ b/application/en/routing.texy @@ -3,27 +3,26 @@ Routing
    -The router is responsible for everything about URLs so that you no longer have to think about them. We will show: +The router handles everything related to URL addresses, so you don't have to think about them. We will show you: -- how to set up the router so that the URLs look like you want -- a few notes about SEO redirection -- and we'll show you how to write your own router +- how to configure the router to make URLs look as you wish +- discuss SEO and redirection +- and demonstrate how to write a custom router
    -More human URLs (or cool or pretty URLs) are more usable, more memorable and contribute positively to SEO. Nette has this in mind and fully meets developers' desires. You can design your URL structure for your application exactly the way you want it. -You can even design it after the app is ready, as it can be done without any code or template changes. It is defined in an elegant way in [one single place |#Integration], in the router, and is not scattered in the form of annotations in all presenters. +More human-friendly URLs (also known as cool or pretty URLs) are more usable, memorable, and contribute positively to SEO. Nette keeps this in mind and fully caters to developers' needs. You can design the exact URL structure you want for your application. You can even design it when the application is already finished, as it requires no changes to code or templates. It's defined elegantly in [a single place |#Integration], the router, rather than being scattered as annotations throughout all presenters. -The router in Nette is special because it is **bidirectional**, it can both decode HTTP request URLs as well as create links. So it plays a vital role in [Nette Application |how-it-works#Nette Application], because it decides which presenter and action will execute the current request, and is also used for [URL generation |creating-links] in the template, etc. +The router in Nette is exceptional because it is **bidirectional.** It can both decode URLs from HTTP requests and create links. Thus, it plays a crucial role in [Nette Application |how-it-works#Nette Application], as it not only decides which presenter and action will execute the current request but is also used for [generating URLs |creating-links] in templates, etc. -However, the router is not limited to this use, you can use it in applications where presenters are not used at all, for REST APIs, etc. More in the section [#separated usage]. +However, the router isn't limited to just this usage; you can use it in applications where presenters aren't used at all, for REST APIs, etc. More details are in the section on [#Standalone Usage]. Route Collection ================ -The most pleasant way to define the URL addresses in the application is via the class [api:Nette\Application\Routers\RouteList]. The definition consists of a list of so-called routes, ie masks of URL addresses and their associated presenters and actions using a simple API. We do not have to name the routes. +The most pleasant way to define the structure of URL addresses in an application is offered by the [api:Nette\Application\Routers\RouteList] class. The definition consists of a list of so-called routes, i.e., masks of URL addresses and their associated presenters and actions using a simple API. We don't need to name the routes in any way. ```php $router = new Nette\Application\Routers\RouteList; @@ -32,19 +31,16 @@ $router->addRoute('article/', 'Article:view'); // ... ``` -The example says that if we open `https://any-domain.com/rss.xml` in the browser, the presenter `Feed` with the action `rss` will be displayed, if `https://domain.com/article/12`, the `Article` with the `view` action is displayed, etc. If no suitable route is found, Nette Application responds by throwing an exception [BadRequestException |api:Nette\Application\BadRequestException], which appears to the user as a 404 Not Found error page. - -.[note] -In Nette 2.x, `$router[] = new Route(...)` was used instead of `$router->addRoute(...)`. +The example shows that if we open `https://domain.com/rss.xml` in the browser, the `Feed` presenter with the `rss` action will be displayed. If `https://domain.com/article/12`, the `Article` presenter with the `view` action will be displayed, etc. If no suitable route is found, Nette Application responds by throwing a [BadRequestException |api:Nette\Application\BadRequestException], which is displayed to the user as a 404 Not Found error page. Order of Routes --------------- -The order in which the routes are listed is **very important** because they are evaluated sequentially from top to bottom. The rule is that we declare routes **from specific to general**: +The **order** in which the individual routes are listed is absolutely **crucial**, because they are evaluated sequentially from top to bottom. The rule is that we declare routes **from specific to general**: ```php -// WRONG: 'rss.xml' matches the first route and misunderstands this as +// WRONG: 'rss.xml' is captured by the first route and understands this string as $router->addRoute('', 'Article:view'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -53,10 +49,10 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('', 'Article:view'); ``` -Routes are also evaluated from top to bottom when links are generated: +Routes are also evaluated from top to bottom when generating links: ```php -// WRONG: generates a link to 'Feed:rss' as 'admin/feed/rss' +// WRONG: link to 'Feed:rss' generates as 'admin/feed/rss' $router->addRoute('admin//', 'Admin:default'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -65,57 +61,57 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('admin//', 'Admin:default'); ``` -We won't keep it a secret from you that it takes some skill to build a list correctly. Until you get into it, the [routing panel |#Debugging Router] will be a useful tool. +We won't hide from you that correctly assembling routes requires some skill. Until you master it, the [routing panel |#Debugging Router] will be a useful tool. Mask and Parameters ------------------- -The mask describes the relative path based on the site root. The simplest mask is a static URL: +The mask describes the relative path from the website's root directory. The simplest mask is a static URL: ```php $router->addRoute('products', 'Products:default'); ``` -Often masks contain so-called **parameters**. They are enclosed in angle brackets (e.g. ``) and are passed to the target presenter, for example to the `renderShow(int $year)` method or to persistent parameter `$year`: +Often, masks contain so-called **parameters**. These are enclosed in angle brackets (e.g., ``) and are passed to the target presenter, for example, to the `renderShow(int $year)` method or to the persistent parameter `$year`: ```php $router->addRoute('chronicle/', 'History:show'); ``` -The example says that if we open `https://any-domain.com/chronicle/2020` in the browser, the presenter `History` and the action `show` with parameter `year: 2020` will be displayed. +The example shows that if we open `https://example.com/chronicle/2020` in the browser, the `History` presenter with the `show` action and the parameter `year: 2020` will be displayed. -We can specify a default value for the parameters directly in the mask and thus it becomes optional: +We can specify a default value for parameters directly in the mask, making them optional: ```php $router->addRoute('chronicle/', 'History:show'); ``` -The route will now accept the URL `https://any-domain.com/chronicle/`, which will again display `History:show` with parameter `year: 2020`. +The route will now also accept the URL `https://example.com/chronicle/`, which will again display `History:show` with the parameter `year: 2020`. -Of course, the name of the presenter and the action can also be a parameter. For example: +Of course, the presenter and action names can also be parameters. For example: ```php -$router->addRoute('/', 'Homepage:default'); +$router->addRoute('/', 'Home:default'); ``` -This route accepts, for example, a URL in the form `/article/edit` resp. `/catalog/list` and translates them to presenters and actions `Article:edit` resp. `Catalog:list`. +The specified route accepts, for example, URLs in the form `/article/edit` or `/catalog/list` and understands them as presenters and actions `Article:edit` and `Catalog:list`, respectively. -It also gives to parameters `presenter` and `action` default values ​​`Homepage` and `default` and therefore they are optional. So the route also accepts a URL `/article` and translates it as `Article:default`. Or vice versa, a link to `Product:default` generates a path `/product`, a link to the default `Homepage:default` generates a path `/`. +At the same time, it gives the parameters `presenter` and `action` default values `Home` and `default`, making them optional as well. Thus, the route also accepts a URL like `/article` and understands it as `Article:default`. Or conversely, a link to `Product:default` generates the path `/product`, and a link to the default `Home:default` generates the path `/`. -The mask can describe not only the relative path based on the site root, but also the absolute path when it begins with a slash, or even the entire absolute URL when it begins with two slashes: +The mask can describe not only the relative path from the website's root directory but also an absolute path if it starts with a slash, or even the entire absolute URL if it starts with two slashes: ```php -// relative path to application document root +// relative to the document root $router->addRoute('/', /* ... */); -// absolute path, relative to server hostname +// absolute path (relative to the domain) $router->addRoute('//', /* ... */); -// absolute URL including hostname (but scheme-relative) +// absolute URL including domain (relative to the scheme) $router->addRoute('//.example.com//', /* ... */); -// absolute URL including schema +// absolute URL including scheme $router->addRoute('https://.example.com//', /* ... */); ``` @@ -123,16 +119,16 @@ $router->addRoute('https://.example.com//', /* ... */); Validation Expressions ---------------------- -A validation condition can be specified for each parameter using [regular expression |https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. For example, let's set `id` to be only numerical, using `\d+` regexp: +A validation condition can be specified for each parameter using a [regular expression|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. For example, for the parameter `id`, we specify that it can only contain digits using the regex `\d+`: ```php $router->addRoute('/[/]', /* ... */); ``` -The default regular expression for all parameters is `[^/]+`, ie everything except the slash. If a parameter is supposed to match a slash as well, we set the regular expression to `.+`. +The default regular expression for all parameters is `[^/]+`, i.e., everything except a slash. If a parameter is supposed to accept slashes as well, we set the expression to `.+`: ```php -// accepts https://example.com/a/b/c, path is 'a/b/c' +// accepts https://example.com/a/b/c, path will be 'a/b/c' $router->addRoute('', /* ... */); ``` @@ -140,74 +136,74 @@ $router->addRoute('', /* ... */); Optional Sequences ------------------ -Square brackets denote optional parts of mask. Any part of mask may be set as optional, including those containing parameters: +In the mask, optional parts can be marked using square brackets. Any part of the mask can be optional, and it can contain parameters: ```php $router->addRoute('[/]', /* ... */); -// Accepted URLs: Parameters: -// /en/download lang => en, name => download -// /download lang => null, name => download +// Accepts paths: +// /en/download => lang => en, name => download +// /download => lang => null, name => download ``` -Of course, when a parameter is part of an optional sequence, it also becomes optional. If it does not have a default value, it will be null. +When a parameter is part of an optional sequence, it naturally becomes optional too. If it doesn't have a specified default value, it will be null. -Optional sections can also be in the domain: +Optional parts can also be in the domain: ```php $router->addRoute('//[.]example.com//', /* ... */); ``` -Sequences may be freely nested and combined: +Sequences can be nested and combined arbitrarily: ```php $router->addRoute( '[[-]/][/page-]', - 'Homepage:default' + 'Home:default', ); -// Accepted URLs: -// /cs/hello -// /en-us/hello -// /hello -// /hello/page-12 +// Accepts paths: +// /en/hello +// /en-us/hello +// /hello +// /hello/page-12 ``` -URL generator tries to keep the URL as short as possible, so what can be omitted is omitted. Therefore, for example, a route `index[.html]` generates a path `/index`. You can reverse this behavior by writing an exclamation mark after the left square bracket: +When generating URLs, the shortest variant is preferred, so everything that can be omitted is omitted. Therefore, for example, the route `index[.html]` generates the path `/index`. This behavior can be reversed by placing an exclamation mark after the left square bracket: ```php -// accepts both /hello and /hello.html, generates /hello +// accepts /hello and /hello.html, generates /hello $router->addRoute('[.html]', /* ... */); -// accepts both /hello and /hello.html, generates /hello.html +// accepts /hello and /hello.html, generates /hello.html $router->addRoute('[!.html]', /* ... */); ``` -Optional parameters (ie. parameters having default value) without square brackets do behave as if wrapped like this: +Optional parameters (i.e., parameters with a default value) without square brackets essentially behave as if they were enclosed in the following way: ```php -$router->addRoute('//', /* ... */); +$router->addRoute('//', /* ... */); -// equals to: -$router->addRoute('[/[/[]]]', /* ... */); +// corresponds to this: +$router->addRoute('[/[/[]]]', /* ... */); ``` -To change how the rightmost slash is generated, i.e. instead of `/homepage/` get a `/homepage`, adjust the route this way: +If we want to influence the behavior of the trailing slash, so that, for example, `/home` is generated instead of `/home/`, this can be achieved as follows: ```php -$router->addRoute('[[/[/]]]', /* ... */); +$router->addRoute('[[/[/]]]', /* ... */); ``` Wildcards --------- -In the absolute path mask, we can use the following wildcards to avoid, for example, the need to write a domain to the mask, which may differ in the development and production environment: +In the absolute path mask, we can use the following wildcards to avoid, for example, having to write the domain into the mask, which might differ between development and production environments: -- `%tld%` = top level domain, e.g. `com` or `org` -- `%sld%` = second level domain, e.g. `example` -- `%domain%` = domain without subdomains, e.g. `example.com` -- `%host%` = whole host, e.g. `www.example.com` +- `%tld%` = top level domain, e.g., `com` or `org` +- `%sld%` = second level domain, e.g., `example` +- `%domain%` = domain without subdomains, e.g., `example.com` +- `%host%` = entire host, e.g., `www.example.com` - `%basePath%` = path to the root directory ```php @@ -219,54 +215,54 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ - 'presenter' => 'Homepage', + 'presenter' => 'Home', 'action' => 'default', ]); ``` -Or we can use this form, notice the rewriting of the validation regular expression: +For more detailed specification, an even more extended form can be used, where besides default values, we can set other parameter properties, such as a validation regular expression (see the `id` parameter): ```php use Nette\Routing\Route; $router->addRoute('/[/]', [ 'presenter' => [ - Route::VALUE => 'Homepage', + Route::Value => 'Home', ], 'action' => [ - Route::VALUE => 'default', + Route::Value => 'default', ], 'id' => [ - Route::PATTERN => '\d+', + Route::Pattern => '\d+', ], ]); ``` -These more talkative formats are useful for adding other metadata. +It is important to note that if parameters defined in the array are not listed in the path mask, their values cannot be changed, not even using query parameters specified after the question mark in the URL. Filters and Translations ------------------------ -It's a good practice to write source code in English, but what if you need your website to have translated URL to different language? Simple routes such as: +We write the application's source code in English, but if the website needs to have Czech URLs, then simple routing like: ```php -$router->addRoute('/', 'Homepage:default'); +$router->addRoute('/', 'Home:default'); ``` -will generate English URLs, such as `/product/123` or `/cart`. If we want to have presenters and actions in the URL translated to Deutsch (e.g. `/produkt/123` or `/einkaufswagen`), we can use a translation dictionary. To add it, we already need a "more talkative" variant of the second parameter: +will generate English URLs, such as `/product/123` or `/cart`. If we want presenters and actions in the URL to be represented by Czech words (e.g., `/produkt/123` or `/kosik`), we can use a translation dictionary. To write it, we already need the "more verbose" variant of the second parameter: ```php use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => [ - Route::VALUE => 'Homepage', - Route::FILTER_TABLE => [ + Route::Value => 'Home', + Route::FilterTable => [ // string in URL => presenter 'produkt' => 'Product', 'einkaufswagen' => 'Cart', @@ -274,66 +270,66 @@ $router->addRoute('/', [ ], ], 'action' => [ - Route::VALUE => 'default', - Route::FILTER_TABLE => [ + Route::Value => 'default', + Route::FilterTable => [ 'liste' => 'list', ], ], ]); ``` -Multiple dictionary keys can by used for the same presenter. They will create various aliases for it. The last key is considered to be the canonical variant (i.e. the one that will be in the generated URL). +Multiple keys in the translation dictionary can lead to the same presenter. This creates various aliases for it. The last key is considered the canonical variant (i.e., the one that will be in the generated URL). -The translation table can be applied to any parameter in this way. However, if the translation does not exist, the original value is taken. We can change this behavior by adding `Route::FILTER_STRICT => true` and the route will then reject the URL if the value is not in the dictionary. +The translation table can be used in this way for any parameter. If a translation doesn't exist, the original value is taken. We can change this behavior by adding `Route::FilterStrict => true`, and the route will then reject the URL if the value is not in the dictionary. -In addition to the translation dictionary in the form of an array, it is possible to set own translation functions: +In addition to the translation dictionary in the form of an array, custom translation functions can be deployed. ```php use Nette\Routing\Route; $router->addRoute('//', [ 'presenter' => [ - Route::VALUE => 'Homepage', - Route::FILTER_IN => function (string $s): string { /* ... */ }, - Route::FILTER_OUT => function (string $s): string { /* ... */ }, + Route::Value => 'Home', + Route::FilterIn => function (string $s): string { /* ... */ }, + Route::FilterOut => function (string $s): string { /* ... */ }, ], 'action' => 'default', 'id' => null, ]); ``` -The function `Route::FILTER_IN` converts between the parameter in the URL and the string, which is then passed to the presenter, the function `FILTER_OUT` ensures the conversion in the opposite direction. +The `Route::FilterIn` function converts between the parameter in the URL and the string that is then passed to the presenter; the `FilterOut` function ensures the conversion in the opposite direction. -The parameters `presenter`, `action` and `module` already have predefined filters that convert between the PascalCase resp. camelCase style and kebab-case used in the URL. The default value of the parameters is already written in the transformed form, so, for example, in the case of a presenter, we write `` instead of ``. +The parameters `presenter`, `action`, and `module` already have predefined filters that convert between PascalCase or camelCase style and the kebab-case used in URLs. The default value of the parameters is written in the transformed form, so for example, in the case of a presenter, we write ``, not ``. General Filters --------------- -Besides filters for specific parameters, you can also define general filters that receive an associative array of all parameters that they can modify in any way and then return. General filters are defined under `null` key. +Besides filters intended for specific parameters, we can also define general filters that receive an associative array of all parameters, which they can modify in any way and then return. General filters are defined under the key `null`. ```php use Nette\Routing\Route; $router->addRoute('/', [ - 'presenter' => 'Homepage', + 'presenter' => 'Home', 'action' => 'default', null => [ - Route::FILTER_IN => function (array $params): array { /* ... */ }, - Route::FILTER_OUT => function (array $params): array { /* ... */ }, + Route::FilterIn => function (array $params): array { /* ... */ }, + Route::FilterOut => function (array $params): array { /* ... */ }, ], ]); ``` -General filters give you the ability to adjust the behavior of the route in absolutely any way. We can use them, for example, to modify parameters based on other parameters. For example, translation `` and `` based on the current value of parameter ``. +General filters provide the ability to modify the route's behavior in absolutely any way. We can use them, for example, to modify parameters based on other parameters. For instance, translating `` and `` based on the current value of the `` parameter. -If a parameter has a custom filter defined and a general filter exists at the same time, custom `FILTER_IN` is executed before the general and vice versa general `FILTER_OUT` is executed before the custom. Thus, inside the general filter are the values of the parameters `presenter` resp. `action` written in PascalCase resp. camelCase style. +If a parameter has its own filter defined and a general filter also exists, the custom `FilterIn` is executed before the general one, and conversely, the general `FilterOut` is executed before the custom one. Thus, inside the general filter, the values of the parameters `presenter` and `action` are written in PascalCase or camelCase style, respectively. -ONE_WAY Flag ------------- +OneWay Flag +----------- -One-way routes are used to preserve the functionality of old URLs that the application no longer generates but still accepts. We flag them with `ONE_WAY`: +One-way routes are used to maintain the functionality of old URLs that the application no longer generates but still accepts. We mark them with the `OneWay` flag: ```php // old URL /product-info?id=123 @@ -342,21 +338,44 @@ $router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); $router->addRoute('product/', 'Product:detail'); ``` -When accessing the old URL, the presenter automatically redirects to the new URL so that search engines do not index these pages twice (see [#SEO and canonization]). +When accessing the old URL, the presenter automatically redirects to the new URL, so search engines won't index these pages twice (see [#SEO and Canonization]). + + +Dynamic Routing with Callbacks +------------------------------ + +Dynamic routing with callbacks allows you to directly assign functions (callbacks) to routes, which are executed when the given path is visited. This flexible functionality allows you to quickly and efficiently create various endpoints for your application: + +```php +$router->addRoute('test', function () { + echo 'You are at the /test address'; +}); +``` + +You can also define parameters in the mask, which are automatically passed to your callback: + +```php +$router->addRoute('', function (string $lang) { + echo match ($lang) { + 'cs' => 'Welcome to the Czech version of our website!', + 'en' => 'Welcome to the English version of our website!', + }; +}); +``` Modules ------- -If we have more routes that belong to one [module |modules], we can use `withModule()` to group them: +If we have multiple routes that belong to a common [module |directory-structure#Presenters and Templates], we use `withModule()`: ```php $router = new RouteList; -$router->withModule('Forum') // the following routers are part of the Forum module - ->addRoute('rss', 'Feed:rss') // presenter is Forum:Feed +$router->withModule('Forum') // the following routes are part of the Forum module + ->addRoute('rss', 'Feed:rss') // presenter will be Forum:Feed ->addRoute('/') - ->withModule('Admin') // the following routers are part of the Forum:Admin module + ->withModule('Admin') // the following routes are part of the Forum:Admin module ->addRoute('sign:in', 'Sign:in'); ``` @@ -365,7 +384,7 @@ An alternative is to use the `module` parameter: ```php // URL manage/dashboard/default maps to presenter Admin:Dashboard $router->addRoute('manage//', [ - 'module' => 'Admin' + 'module' => 'Admin', ]); ``` @@ -373,7 +392,7 @@ $router->addRoute('manage//', [ Subdomains ---------- -Route collections can be grouped by subdomains: +Route collections can be divided according to subdomains: ```php $router = new RouteList; @@ -382,7 +401,7 @@ $router->withDomain('example.com') ->addRoute('/'); ``` -You can also use [#wildcards] in your domain name: +[#Wildcards] can also be used in the domain name: ```php $router = new RouteList; @@ -394,7 +413,7 @@ $router->withDomain('example.%tld%') Path Prefix ----------- -Route collections can be grouped by path in URL: +Route collections can be divided according to the path in the URL: ```php $router = new RouteList; @@ -407,7 +426,7 @@ $router->withPath('eshop') Combinations ------------ -The above usage can be combined: +The above groupings can be combined with each other: ```php $router = (new RouteList) @@ -430,10 +449,10 @@ $router = (new RouteList) Query Parameters ---------------- -Masks can also contain query parameters (parameters after the question mark in the URL). They cannot define a validation expression, but they can change the name under which they are passed to the presenter: +Masks can also contain query parameters (parameters after the question mark in the URL). A validation expression cannot be defined for these, but the name under which they are passed to the presenter can be changed: ```php -// use query parameter 'cat' as a 'categoryId' in application +// we want to use the query parameter 'cat' under the name 'categoryId' in the application $router->addRoute('product ? id= & cat=', /* ... */); ``` @@ -441,13 +460,13 @@ $router->addRoute('product ? id= & cat=', /* ... */); Foo Parameters -------------- -We're going deeper now. Foo parameters are basically unnamed parameters which allow to match a regular expression. The following route matches `/index`, `/index.html`, `/index.htm` and `/index.php`: +Now we're going deeper. Foo parameters are essentially unnamed parameters that allow matching a regular expression. An example is a route accepting `/index`, `/index.html`, `/index.htm`, and `/index.php`: ```php $router->addRoute('index', /* ... */); ``` -It's also possible to explicitly define a string which will be used for URL generation. The string must be placed directly after the question mark. The following route is similar to the previous one, but generates `/index.html` instead of `/index` because the string `.html` is set as a "generated value". +It is also possible to explicitly define the string that will be used when generating the URL. The string must be placed directly after the question mark. The following route is similar to the previous one, but generates `/index.html` instead of `/index`, because the string `.html` is set as the generation value: ```php $router->addRoute('index', /* ... */); @@ -457,10 +476,10 @@ $router->addRoute('index', /* ... */); Integration =========== -In order to connect the our router into the application, we must tell the DI container about it. The easiest way is to prepare the factory that will build the router object and tell the container configuration to use it. So let's say we write a method for this purpose `App\Router\RouterFactory::createRouter()`: +To integrate the created router into the application, we need to tell the DI container about it. The easiest way is to prepare a factory that will create the router object and tell the container in the configuration to use it. Let's say we write the method `App\Core\RouterFactory::createRouter()` for this purpose: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -475,14 +494,14 @@ class RouterFactory } ``` -Then we write in [configuration |dependency-injection:services]: +Then we write in the [configuration |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` -Any dependencies, such as a database connection etc., are passed to the factory method as its parameters using [autowiring |dependency-injection:autowiring]: +Any dependencies, such as on a database, etc., are passed to the factory method as its parameters using [autowiring|dependency-injection:autowiring]: ```php public static function createRouter(Nette\Database\Connection $db): RouteList @@ -495,35 +514,35 @@ public static function createRouter(Nette\Database\Connection $db): RouteList SimpleRouter ============ -A much simpler router than the Route Collection is [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. It can be used when there's no need for a specific URL format, when `mod_rewrite` (or alternatives) is not available or when we simply do not want to bother with user-friendly URLs yet. +A much simpler router than the route collection is [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. We use it when we don't have special requirements for the URL format, if `mod_rewrite` (or its alternatives) is not available, or if we don't want to deal with pretty URLs yet. -Generates addresses in roughly this form: +It generates addresses roughly in this form: ``` http://example.com/?presenter=Product&action=detail&id=123 ``` -The parameter of the `SimpleRouter` constructor is a default presenter & action, ie. action to be executed if we open e.g. `http://example.com/` without additional parameters. +The parameter of the `SimpleRouter` constructor is a default presenter & action, i.e. action to be executed if we open e.g. `http://example.com/` without additional parameters. ```php -// defaults to presenter 'Homepage' and action 'default' -$router = new Nette\Application\Routers\SimpleRouter('Homepage:default'); +// the default presenter will be 'Home' and action 'default' +$router = new Nette\Application\Routers\SimpleRouter('Home:default'); ``` We recommend defining SimpleRouter directly in [configuration |dependency-injection:services]: ```neon services: - - Nette\Application\Routers\SimpleRouter('Homepage:default') + - Nette\Application\Routers\SimpleRouter('Home:default') ``` SEO and Canonization ==================== -The framework increases SEO (search engine optimization) by preventing duplication of content at different URLs. If multiple addresses link to a same destination, eg `/index` and `/index.html`, the framework determines the first one as primary (canonical) and redirects the others to it using HTTP code 301. Thanks to this, search engines will not index pages twice and do not break their page rank. . +The framework contributes to SEO (Search Engine Optimization) by preventing duplicate content on different URLs. If multiple addresses lead to a certain destination, e.g., `/index` and `/index.html`, the framework designates the first one as primary (canonical) and redirects the others to it using HTTP code 301. Thanks to this, search engines do not index pages twice and do not dilute their page rank. -This process is called canonization. The canonical URL is the one generated by the router, i.e. by the first matching route in the [collection |#route-collection] without the ONE_WAY flag. Therefore, in the collection, we list **primary routes first**. +This process is called canonization. The canonical URL is the one generated by the router, i.e. by the first matching route in the collection without the OneWay flag. Therefore, in the collection, we list **primary routes first**. Canonization is performed by the presenter, more in the chapter [canonization |presenters#Canonization]. @@ -531,9 +550,9 @@ Canonization is performed by the presenter, more in the chapter [canonization |p HTTPS ===== -In order to use the HTTPS protocol, it is necessary to activate it on hosting and to configure the server. +To use the HTTPS protocol, it is necessary to enable it on the hosting and configure the server correctly. -Redirection of the entire site to HTTPS must be performed at the server level, for example using the .htaccess file in the root directory of our application, with HTTP code 301. The settings may differ depending on the hosting and looks something like this: +Redirecting the entire website to HTTPS must be set at the server level, for example, using the `.htaccess` file in the root directory of our application, with HTTP code 301. The settings may vary depending on the hosting and look something like this: ``` @@ -545,9 +564,9 @@ Redirection of the entire site to HTTPS must be performed at the server level, f ``` -The router generates a URL with the same protocol as the page was loaded, so there is no need to set anything else. +The router generates URLs with the same protocol as the page was loaded, so nothing more needs to be set. -However, if we exceptionally need different routes to run under different protocols, we will put it in the route mask: +However, if we exceptionally need different routes to run under different protocols, we specify it in the route mask: ```php // Will generate an HTTP address @@ -561,24 +580,24 @@ $router->addRoute('https://%host%//', /* ... */); Debugging Router ================ -The routing bar displayed in [Tracy Bar |tracy:] is a useful tool that displays a list of routes and also the parameters that the router has obtained from the URL. +The routing panel displayed in the [Tracy Bar |tracy:] is a useful helper that shows a list of routes and also the parameters that the router obtained from the URL. -The green bar with symbol ✓ represents the route that matched the current URL, the blue bars with symbols ≈ indicate the routes that would also match the URL if green did not overtake them. We see the current presenter & action further. +The green bar with the symbol ✓ represents the route that processed the current URL; blue color and the symbol ≈ indicate routes that would also process the URL if the green one hadn't overtaken them. Further, we see the current presenter & action. [* routing-debugger.webp *] -At the same time, if there is an unexpected redirect due to [canonicalization |#SEO and Canonization], it is useful to look in the *redirect* bar to see how the router originally understood the URL and why it redirected. +At the same time, if an unexpected redirect occurs due to [canonization |#SEO and Canonization], it is useful to look at the panel in the *redirect* bar, where you can find out how the router originally understood the URL and why it redirected. .[note] -When debugging the router, it is recommended to open Developer Tools in the browser (Ctrl+Shift+I or Cmd+Option+I) and disable the cache in the Network panel so that redirects are not stored in it. +When debugging the router, we recommend opening Developer Tools in the browser (Ctrl+Shift+I or Cmd+Option+I) and disabling the cache in the Network panel so that redirects are not stored in it. Performance =========== -The number of routes affects the speed of the router. Their number should certainly not exceed a few dozen. If your site has an overly complicated URL structure, you can write a [#custom router]. +The number of routes affects the speed of the router. Their number should definitely not exceed several dozen. If your website has a too complicated URL structure, you can write a custom [#Custom Router]. -If the router has no dependencies, such as on a database, and its factory has no arguments, we can serialize its compiled form directly into a DI container and thus make the application slightly faster. +If the router has no dependencies, for example, on a database, and its factory accepts no arguments, we can serialize its compiled form directly into the DI container and thus slightly speed up the application. ```neon routing: @@ -589,7 +608,7 @@ routing: Custom Router ============= -The following lines are intended for very advanced users. You can create your own router and naturally add it into your route collection. The router is an implementation of the [api:Nette\Routing\Router] interface with two methods: +The following lines are intended for very advanced users. You can create your own router and naturally integrate it into the collection of routes. The router is an implementation of the [api:Nette\Routing\Router] interface with two methods: ```php use Nette\Http\IRequest as HttpRequest; @@ -609,19 +628,18 @@ class MyRouter implements Nette\Routing\Router } ``` -The `match` method processes the current [$httpRequest |http:request], from which not only the URL, but also headers etc. can be retrieved, into an array containing the presenter name and its parameters. If it cannot process the request, it returns null. -When processing the request, we must return at least the presenter and the action. The presenter name is complete and includes any modules: +The `match` method processes the current request [$httpRequest |http:request], from which not only the URL but also headers, etc., can be obtained, into an array containing the presenter name and its parameters. If it cannot process the request, it returns null. When processing the request, we must return at least the presenter and action. The presenter name is complete and includes any modules: ```php [ - 'presenter' => 'Front:Homepage', + 'presenter' => 'Front:Home', 'action' => 'default', ] ``` -Method `constructUrl`, on the other hand, generates an absolute URL from the array of parameters. It can use the information from parameter `$refUrl`, which is the current URL. +The `constructUrl` method, on the contrary, constructs the resulting absolute URL from the array of parameters. It can use information from the [`$refUrl`|api:Nette\Http\UrlScript] parameter, which is the current URL. -To add custom router to the route collection, use `add()`: +Add it to the route collection using `add()`: ```php $router = new Nette\Application\Routers\RouteList; @@ -631,19 +649,19 @@ $router->addRoute(/* ... */); ``` -Separated Usage -=============== +Standalone Usage +================ -By separated usage, we mean the use of the router's capabilities in an application that does not use Nette Application and presenters. Almost everything we have shown in this chapter applies to it, with the following differences: +By standalone usage, we mean utilizing the router's capabilities in an application that does not use Nette Application and presenters. Almost everything we have shown in this chapter applies to it, with these differences: -- for route collections we use class [api:Nette\Routing\RouteList] -- as a simple router class [api:Nette\Routing\SimpleRouter] -- because there is no pair `Presenter:action`, we use [#Advanced notation] +- for route collections, we use the [api:Nette\Routing\RouteList] class +- as a simple router, the [api:Nette\Routing\SimpleRouter] class +- because the `Presenter:action` pair does not exist, we use [#Advanced Notation] -So again we will create a method that will build a router, for example: +So again, we create a method that will assemble the router for us, e.g.: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -664,35 +682,35 @@ class RouterFactory } ``` -If you use a DI container, which we recommend, add the method to the configuration again and then get the router together with the HTTP request from the container: +If you use a DI container, which we recommend, add the method to the configuration again, and then obtain the router along with the HTTP request from the container: ```php $router = $container->getByType(Nette\Routing\Router::class); $httpRequest = $container->getByType(Nette\Http\IRequest::class); ``` -Or we will create objects directly: +Or create the objects directly: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` -Now we have to let the router to work: +Now all that remains is to let the router do its work: ```php $params = $router->match($httpRequest); if ($params === null) { - // no matching route found, we will send a 404 error + // no matching route found, send a 404 error exit; } -// we process the received parameters +// process the obtained parameters $controller = $params['controller']; // ... ``` -And vice versa, we will use the router to create the link: +And conversely, use the router to construct a link: ```php $params = ['controller' => 'ArticleController', 'id' => 123]; diff --git a/application/en/templates.texy b/application/en/templates.texy index aaed352bd6..2950a8f35c 100644 --- a/application/en/templates.texy +++ b/application/en/templates.texy @@ -2,9 +2,9 @@ Templates ********* .[perex] -Nette uses the [Latte |latte:] template system. Latte is used because it is the most secure template system for PHP, and at the same time the most intuitive system. You don't have to learn much new, you just need to know PHP and a few Latte tags. +Nette uses the [Latte |latte:] templating system. Latte is used because it is the most secure templating system for PHP, and at the same time, the most intuitive system. You don't need to learn much new; knowledge of PHP and a few tags will suffice. -It is usual that the page is completed from the layout template + the action template. This is what a layout template might look like, notice the blocks `{block}` and tag `{include}`: +It's common for a page to be composed of a layout template + the template for the specific action. This is what a layout template might look like; notice the `{block}` blocks and the `{include}` tag: ```latte @@ -20,7 +20,7 @@ It is usual that the page is completed from the layout template + the action tem ``` -And this might be the action template: +And this would be the action template: ```latte {block title}Homepage{/block} @@ -31,50 +31,98 @@ And this might be the action template: {/block} ``` -It defines block `content`, which is inserted in place of `{include content}` in the layout, and also re-defines block `title`, which overwrites `{block title}` in the layout. Try to imagine the result. +It defines the `content` block, which is inserted in place of `{include content}` in the layout, and also re-defines the `title` block, which overwrites `{block title}` in the layout. Try to imagine the result. -Search for Templates --------------------- +Template Lookup +--------------- -The path to the templates is deduced according to simple logic. It tries to see if one of these template files exists relative to the directory where presenter class is located, where `` is the name of the current presenter and `` is the name of the current action: +In presenters, you don't need to specify which template should be rendered; the framework automatically deduces the path, saving you from writing it. -- `templates//.latte` -- `templates/..latte` +If you use a directory structure where each presenter has its own directory, simply place the template in this directory under the name of the action (i.e., view). For example, for the `default` action, use the `default.latte` template: -If it does not find the template, the response is [error 404 |presenters#Error 404 etc.]. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -You can also change the view using `$this->setView('otherView')`. Or, instead of searching, directly specify the name of the template file using `$this->template->setFile('/path/to/template.latte')`. +If you use a structure where presenters are together in one directory and templates are in a `templates` folder, save it either in the file `..latte` or `/.latte`: + +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- + +The `templates` directory can also be placed one level higher, i.e., at the same level as the directory with presenter classes. + +If the template is not found, the presenter responds with a [404 - page not found error |presenters#Error 404 etc]. + +You can change the view using `$this->setView('otherView')`. It is also possible to directly specify the template file using `$this->template->setFile('/path/to/template.latte')`. .[note] -You can change the paths where templates are searched by overriding the [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()] method, which returns an array of possible file paths. +The files where templates are looked up can be changed by overriding the [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] method, which returns an array of possible file names. + + +Layout Template Lookup +---------------------- -The layout is expected in the following files: +Nette also automatically searches for the layout file. -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` layout common to multiple presenters +If you use a directory structure where each presenter has its own directory, place the layout either in the folder with the presenter, if it is specific only to it, or one level higher, if it is common to multiple presenters: -`` is the name of the current presenter and `` is the name of the layout, which is by default `'layout'`. The name can be changed with `$this->setLayout('otherLayout')`, so that `@otherLayout.latte` files will be tried. +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- -You can also directly specify the file name of the layout template using `$this->setLayout('/path/to/template.latte')`. Using `$this->setLayout(false)` will disable the layout searching. +If you use a structure where presenters are grouped together in one directory and templates are in a `templates` folder, the layout will be expected in these locations: + +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- + +If the presenter is located in a module, it will also search further up the directory levels, according to the module nesting. + +The layout name can be changed using `$this->setLayout('layoutAdmin')`, and then it will be expected in the file `@layoutAdmin.latte`. You can also directly specify the layout template file using `$this->setLayout('/path/to/template.latte')`. + +Using `$this->setLayout(false)` or the `{layout none}` tag inside the template disables layout lookup. .[note] -You can change the paths where templates are searched by overriding the [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] method, which returns an array of possible file paths. +The files where layout templates are looked up can be changed by overriding the [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] method, which returns an array of possible file names. Variables in the Template ------------------------- -Variables are passed to the template by writing them to `$this->template` and then they are available in the template as local variables: +Variables are passed to the template by writing them to `$this->template`, and then they are available in the template as local variables: ```php $this->template->article = $this->articles->getById($id); ``` -In this way we can easily pass any variables to the templates. However, when developing robust applications, it is often more useful to limit ourselves. For example, by explicitly defining a list of variables that the template expects and their types. This will allow PHP to type-check, the IDE to autocomplete correctly, and static analysis to detect errors. +This way, we can easily pass any variables to templates. However, when developing robust applications, it is often more useful to impose limitations. For example, by explicitly defining a list of variables that the template expects and their types. This allows PHP to perform type checking, the IDE to provide correct autocompletion, and static analysis to detect errors. -And how do we define such an enumeration? Simply in the form of a class and its properties. We name it similarly to presenter, but with `Template` at the end: +And how do we define such a list? Simply in the form of a class and its properties. We name it similarly to the presenter, but with `Template` at the end: ```php /** @@ -86,32 +134,29 @@ class ArticlePresenter extends Nette\Application\UI\Presenter class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template { - /** @var Model\Article */ - public $article; - - /** @var Nette\Security\User */ - public $user; + public Model\Article $article; + public Nette\Security\User $user; // and other variables } ``` -The `$this->template` object in the presenter will now be an instance of the `ArticleTemplate` class. So PHP will check the declared types when they are written. And starting with PHP 8.2 it will also warn about writing to a non-existent variable, in previous versions the same can be achieved using the [Nette\SmartObject |utils:smartobject] trait. +The `$this->template` object in the presenter will now be an instance of the `ArticleTemplate` class. So PHP will check the declared types upon writing. And starting from PHP 8.2, it will also warn about writing to a non-existent variable; in previous versions, the same can be achieved using the [Nette\SmartObject |utils:smartobject] trait. -The `@property-read` annotation is for IDE and static analysis, it will make autocomplete work, see "PhpStorm and code completion for $this->template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. +The `@property-read` annotation is intended for IDEs and static analysis; thanks to it, autocompletion will work, see "PhpStorm and code completion for $this->template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. [* phpstorm-completion.webp *] -You can indulge in the luxury of whispering in templates too, just install the Latte plugin in PhpStorm and specify the class name at the beginning of the template, see the article "Latte: how to type system":https://blog.nette.org/en/latte-how-to-use-type-system: +You can enjoy the luxury of autocompletion in templates too; just install the Latte plugin for PhpStorm and specify the class name at the beginning of the template, more in the article "Latte: How to Use Type System":https://blog.nette.org/en/latte-how-to-use-type-system: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` -This is also how templates work in components, just follow the naming convention and create a template class `FifteenTemplate` for the component e.g. `FifteenControl`. +This is also how templates work in components; just follow the naming convention and create a template class `FifteenTemplate` for a component like `FifteenControl`. -If you need to create a `$template` as an instance of another class, use the `createTemplate()` method: +If you need to create `$template` as an instance of another class, use the `createTemplate()` method: ```php public function renderDefault(): void @@ -127,14 +172,14 @@ public function renderDefault(): void Default Variables ----------------- -Presenters and components pass several useful variables to templates automatically: +Presenters and components automatically pass several useful variables to templates: -- `$basePath` is an absolute URL path to root dir (for example `/CD-collection`) -- `$baseUrl` is an absolute URL to root dir (for example `http://localhost/CD-collection`) +- `$basePath` is the absolute URL path to the root directory (e.g., `/eshop`) +- `$baseUrl` is the absolute URL to the root directory (e.g., `http://localhost/eshop`) - `$user` is an object [representing the user |security:authentication] - `$presenter` is the current presenter - `$control` is the current component or presenter -- `$flashes` list of [messages |presenters#flash-messages] sent by method `flashMessage()` +- `$flashes` is an array of [messages |presenters#Flash Messages] sent by the `flashMessage()` function If you use a custom template class, these variables are passed if you create a property for them. @@ -142,19 +187,19 @@ If you use a custom template class, these variables are passed if you create a p Creating Links -------------- -In template we create links to other presenters & actions as follows: +In the template, links to other presenters & actions are created this way: ```latte -detail +product detail ``` -Attribute `n:href` is very handy for HTML tags ``. If we want to print the link elsewhere, for example in the text, we use `{link}`: +The `n:href` attribute is very handy for HTML `` tags. If we want to print the link elsewhere, for example in text, we use `{link}`: ```latte -URL is: {link Homepage:default} +URL is: {link Home:default} ``` -For more information, see [Creating Links]. +More information can be found in the chapter [Creating URL Links|creating-links]. Custom Filters, Tags, etc. @@ -174,10 +219,10 @@ public function beforeRender(): void } ``` -Latte version 3 offers a more advanced way by creating an [extension |latte:creating-extension] for each web project. Here is a rough example of such a class: +Latte version 3 offers a more advanced way by creating an [extension |latte:extending-latte#Latte Extension] for each web project. Here is a brief example of such a class: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -210,10 +255,69 @@ final class LatteExtension extends Latte\Extension } ``` -We register it using [configuration#Latte]: +We register it using [configuration |configuration#Latte Templates]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension +``` + + +Translating +----------- + +If you are programming a multilingual application, you will likely need to output some texts in the template in different languages. Nette Framework defines a translation interface [api:Nette\Localization\Translator] for this purpose, which has a single method `translate()`. It accepts the message `$message`, which is usually a string, and any other parameters. The task is to return the translated string. Nette does not have a default implementation; you can choose from several ready-made solutions available on [Componette |https://componette.org/search/localization] according to your needs. Their documentation explains how to configure the translator. + +Templates can be set up with a translator, which we [get passed |dependency-injection:passing-dependencies], using the `setTranslator()` method: + +```php +protected function beforeRender(): void +{ + // ... + $this->template->setTranslator($translator); +} ``` + +Alternatively, the translator can be set using [configuration |configuration#Latte Templates]: + +```neon +latte: + extensions: + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) +``` + +Then, the translator can be used, for example, as a filter `|translate`, including additional parameters that are passed to the `translate()` method (see `foo, bar`): + +```latte +{='Basket'|translate} +{$item|translate} +{$item|translate, foo, bar} +``` + +Or as an underscore tag: + +```latte +{_'Basket'} +{_$item} +{_$item, foo, bar} +``` + +For translating a section of the template, there is a paired tag `{translate}` (since Latte 2.11, previously the tag `{_}` was used): + +```latte +{translate}Order{/translate} +{translate foo, bar}Order{/translate} +``` + +The translator is normally called at runtime when rendering the template. Latte version 3, however, can translate all static texts already during template compilation. This saves performance, as each string is translated only once, and the resulting translation is written into the compiled form. This creates multiple compiled versions of the template in the cache directory, one for each language. To do this, simply specify the language as the second parameter: + +```php +protected function beforeRender(): void +{ + // ... + $this->template->setTranslator($translator, $lang); +} +``` + +Static text means, for example, `{_'hello'}` or `{translate}hello{/translate}`. Non-static texts, like `{_$foo}`, will continue to be translated at runtime. diff --git a/application/files/request-flow.svg b/application/files/request-flow.svg new file mode 100644 index 0000000000..4df47f2661 --- /dev/null +++ b/application/files/request-flow.svg @@ -0,0 +1,3 @@ + + +
    Front controller
    Front controller
    index.php
    index.php
    Front controller
    Front controller
    index.php
    index.php
    Bootstrap
    Bootstrap
    DI
    Container
    DI...
    Application
    Application
    Nette\Application\Application
    Nette\Appli...
    Routing + Mapping
    Routing + Mapping
    Http
    Request
    Http...
    Presenter
    & params
    Presenter...
    HomePresenter
    HomePresent...
    Presenter
    Presenter
    ContactPresenter
    ContactPres...
    Presenter
    Presenter
    PostPresenter
    PostPresent...
    Presenter
    Presenter
    Response
    Response
    Response
    Response
    Response
    Response
    render template
    render template
    error 404
    error 404
    redirect
    redirect
    Request
    Request
    Request
    Request
    Request
    Request
    Bootstrap.php
    Bootstrap.php
    RouterFactory.php
    + common.neon
    RouterFactory.php...
    URL: /
    URL: /
    URL: /foo
    URL: /foo
    URL: /blog/xxx
    URL: /blog/xxx
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/application/meta.json b/application/meta.json index da1901a760..f6aaeabeb7 100644 --- a/application/meta.json +++ b/application/meta.json @@ -1,5 +1,5 @@ { - "version": "4.0", + "version": "3.x", "repo": "nette/application", "composer": "nette/application" } diff --git a/best-practices/cs/@home.texy b/best-practices/cs/@home.texy deleted file mode 100644 index cd5bcfb847..0000000000 --- a/best-practices/cs/@home.texy +++ /dev/null @@ -1,74 +0,0 @@ -Návody a postupy -**************** - -.[perex] -Návody, řešení častých úloh a *best practices* pro Nette. - - -
    -
    - - -Nette Aplikace --------------- -- [Metody a atributy inject |inject-method-attribute] -- [Skládání presenterů z trait |presenter-traits] -- [Předání nastavení do presenterů |passing-settings-to-presenters] -- [Jak se vrátit k dřívější stránce |restore-request] -- [Stránkování výsledků databáze |pagination] -- [Dynamické snippety |dynamic-snippets] - -
    -
    - - -Formuláře ---------- -- [Formulář pro vytvoření i editaci záznamu |creating-editing-form] -- [Znovupoužití formulářů |form-reuse] -- [Závislé selectboxy |https://blog.nette.org/cs/zavisle-selectboxy-elegantne-v-nette-a-cistem-javascriptu] - -
    -
    - - -Další oblasti -------------- -- [Překládání formulářů a šablon |translations] -- [Jak načíst konfigurační soubor |bootstrap:] - -
    -
    - - -Nástroje --------- -- [Composer: tipy pro použití |composer] -- [Jak používat Code Checker |code-checker:] -- [Tipy na editory & nástroje |editors-and-tools] - -
    -
    - - -Ukázkové řešení ---------------- -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:getting-started] - -
    -
    - - -Videa ------ -Stovky záznamů z Posledních sobot a videí o Nette naleznete pod jednou na "Youtube kanálu Nette Frameworku":https://www.youtube.com/user/NetteFramework. - -
    -
    - -{{sitename: Best Practices}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/cs/composer.texy b/best-practices/cs/composer.texy deleted file mode 100644 index 12ba37407d..0000000000 --- a/best-practices/cs/composer.texy +++ /dev/null @@ -1,248 +0,0 @@ -Composer: tipy pro použití -************************** - -
    - -Composer je nástroj pro správu závislostí v PHP. Umožní nám vyjmenovat knihovny, na kterých náš projekt závisí, a bude je za nás instalovat a aktualizovat. Ukážeme si: - -- jak Composer nainstalovat -- jeho použití v novém či stávajícím projektu - -
    - - -Instalace -========= - -Composer je spustitelný `.phar` soubor, který si stáhnete a nainstalujete následujícím způsobem: - - -Windows -------- - -Použijte oficiální instalátor [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Stačí 4 příkazy, které si zkopírujte z [této stránky |https://getcomposer.org/download/]. - -Dále vložením do složky, která je v systémovém `PATH`, se stane Composer přístupný globálně: - -```shell -$ mv ./composer.phar ~/bin/composer # nebo /usr/local/bin/composer -``` - - -Použití v projektu -================== - -Abychom mohli ve svém projektu začít používat Composer, potřebujete pouze soubor `composer.json`. Ten popisuje závislosti našeho projektu a může také obsahovat další metadata. Základní `composer.json` tedy může vypadat takto: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Říkáme zde, že naše aplikace (nebo knihovna) vyžaduje balíček `nette/database` (název balíčku se skládá z názvu organizace a názvu projektu) a chce verzi, která odpovídá podmínce `^3.0` (tj. nejnovější verzi 3). - -Máme tedy v kořenu projektu soubor `composer.json` a spustíme instalaci: - -```shell -composer update -``` - -Composer stáhne Nette Database do složky `vendor/`. Dále vytvoří soubor `composer.lock`, který obsahuje informace o tom, které verze knihoven přesně nainstaloval. - -Composer vygeneruje soubor `vendor/autoload.php`, který můžeme jednoduše zainkludovat a začít používat knihovny bez jakékoli další práce: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Aktualizace na nejnovější verze -=============================== - -Aktualizaci použiváných knihoven na nejnovější verze podle podmínek definovaných v `composer.json` má na starosti příkaz `composer update`. Např. u závislosti `"nette/database": "^3.0"` nainstaluje nejnovější verzi 3.x.x, ale nikoliv už verzi 4. - -Pro aktualizaci podmínek v souboru `composer.json` například na `"nette/database": "^4.1"`, aby bylo možné nainstalovat nejnovější verzi, použijte příkaz `composer require nette/database`. - -Pro aktualizaci všech používaných balíčků Nette by bylo nutné je všechny v příkazové řádce vyjmenovat, např.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Což je nepraktické. Použijte proto jednoduchý skript "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, který to udělá za vás: - -```shell -php composer-frontline.php -``` - - -Vytvoření nového projektu -========================= - -Nový projekt na Nette vytvoříte pomocí jediného příkazu: - -```shell -composer create-project nette/web-project nazev-projektu -``` - -Jako `nazev-projektu` vložte název adresáře pro svůj projekt a potvrďte. Composer stáhne repozitář `nette/web-project` z GitHubu, který už obsahuje soubor `composer.json`, a hned potom Nette Framework. Mělo by již stačit pouze [nastavit oprávnění |nette:troubleshooting#nastaveni-prav-adresaru] na zápis do složek `temp/` a `log/` a projekt by měl ožít. - - -Verze PHP -========= - -Composer vždy instaluje ty verze balíčků, které jsou kompatibilní s verzí PHP, kterou právě používáte. Což ovšem nemusí být stejná verze, jako používá váš hosting. Proto je užitečné si do souboru `composer.json` přidat informaci o verzi PHP na hostingu a poté se budou instalovat pouze verze balíčků s hostingem kompatibilní: - -```js -{ - "require": { - ... - }, - "config": { - "platform": { - "php": "7.2" # verze PHP na hostingu - } - } -} -``` - - -Packagist.org - centrální repozitář -=================================== - -[Packagist |https://packagist.org] je hlavní repozitář, ve kterém se Composer snaží vyhledávat balíčky, pokud mu neřekneme jinak. Můžeme zde publikovat i vlastní balíčky. - - -Co když nechceme používat centrální repozitář? ----------------------------------------------- - -Pokud máme vnitrofiremní aplikace, které zkrátka nemůžeme hostovat veřejně, tak si pro ně vytvoříme firemní repozitář. - -Více na téma repozitářů [v oficiální dokumentaci |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Zásadní vlastností Composeru je, že poskytuje autoloading pro všechny jím nainstalované třídy, který nastartujete includováním souboru `vendor/autoload.php`. - -Nicméně je možné používat Composer i pro načítání dalších tříd i mimo složku `vendor`. První možností je nechat Composer prohledat definované složky a podsložky, najít všechny třídy a zahrnout je do autoloaderu. Toho docílíte nastavením `autoload > classmap` v `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # zahrne složku src/ a její podsložky - ] - } -} -``` - -Následně je potřeba při každé změně spustit příkaz `composer dumpautoload` a nechat autoloadovací tabulky přegenerovat. To je nesmírně nepohodlné a daleko lepší je tento úkol svěřit [RobotLoaderu|robot-loader:], který stejnou činnost provádí automaticky na pozadí a mnohem rychleji. - -Druhou možností je dodržovat [PSR-4|https://www.php-fig.org/psr/psr-4/]. Zjednodušeně řečeno jde o systém, kdy jmenné prostory a názvy tříd odpovídají adresářové struktuře a názvům souborů, tedy např. `App\Router\RouterFactory` bude v souboru `/path/to/App/Router/RouterFactory.php`. Příklad konfigurace: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # jmenný prostor App\ je v adresáři app/ - } - } -} -``` - -Jak přesně chování nakonfigurovat se dozvíte v [dokumentaci Composeru|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Testování nových verzí -====================== - -Chcete otestovat novou vývojovou verzi balíčku. Jak na to? Nejprve do souboru `composer.json` přidejte tuto dvojici voleb, která dovolí instalovat vývojové verze balíčků, avšak uchýlí se k tomu pouze v případě, že neexistuje žádná kombinace stable verzí, která by vyhovovala požadavkům: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Dále doporučujeme smazat soubor `composer.lock`, někdy totiž Composer nepochopitelně odmítá instalaci a tohle problém vyřeší. - -Dejme tomu, že jde o balíček `nette/utils` a nová verze má číslo 4.0. Nainstalujete ji příkazem: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Nebo můžete nainstalovat konkrétní verzi, například 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Když ale na knihovně závisí jiný balíček, který je uzamčený na starší verzi (např. `^3.1`), tak je ideální balík zaktualizovat, aby s novou verzí fungoval. -Pokud však chcete omezení jen obejít a donutit Composer nainstalovat vývojovou verzi a předstírat, že jde o verzi starší (např. 3.1.6), můžete použít klíčové slovo `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Volání příkazů -============== - -Přes Composer lze volat vlastní předpřipravené příkazy a skripty, jako by šlo o nativní příkazy Composeru. U skriptů, které se nacházejí ve složce `vendor/bin`, není potřeba tuto složku uvádět. - -Jako příklad si definujeme v souboru `composer.json` skript, který pomocí [Nette Testeru|tester:] spustí testy: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Testy pak spustíme pomocí `composer tester`. Příkaz můžeme zavolat i v případě, že nejsme v kořenové složce projektu, ale v některém podadresáři. - - -Pošlete dík -=========== - -Ukážeme vám trik, kterým potěšíte autory open source. Jednoduchým způsobem dáte na GitHubu hvězdičku knihovnám, které váš projekt používá. Stačí nainstalovat knihovnu `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -A poté spustit: - -```shell -composer thanks -``` - -Zkuste si to! - - -Konfigurace -=========== - -Composer je úzce propojený s verzovacím nástrojem [Git |https://git-scm.com]. Pokud jej nemáte nainstalovaný, je třeba Composeru říct, aby jej nepoužíval: - -```shell -composer -g config preferred-install dist -``` - -{{sitename: Best Practices}} diff --git a/best-practices/cs/creating-editing-form.texy b/best-practices/cs/creating-editing-form.texy deleted file mode 100644 index c489deb6de..0000000000 --- a/best-practices/cs/creating-editing-form.texy +++ /dev/null @@ -1,212 +0,0 @@ -Formulář pro vytvoření i editaci záznamu -**************************************** - -.[perex] -Jak správně v Nette implementovat přidání a editaci záznamu, s tím, že pro obojí využijeme tentýž formulář? - -V mnoha případech bývají formuláře pro přidání i editaci záznamu stejné, liší se třeba jen popiskou na tlačítku. Ukážeme příklady jednoduchých presenterů, kde formulář použijeme nejprve pro přidání záznamu, poté pro editaci a nakonec obě řešení spojíme. - - -Přidání záznamu ---------------- - -Příklad presenteru sloužícího k přidání záznamu. Samotnou práci s databází necháme na třídě `Facade`, jejíž kód není pro ukázku podstatný. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $facade; - - public function __construct(Facade $facade) - { - $this->facade = $facade; - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... přidáme políčka formuláře ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // přidání záznamu do databáze - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Editace záznamu ---------------- - -Nyní si ukážeme, jak by vypadal presenter sloužící k editaci záznamu: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $facade; - - private $record; - - public function __construct(Facade $facade) - { - $this->facade = $facade; - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // oveření existence záznamu - || !$this->facade->isEditAllowed(/*...*/) // kontrola oprávnění - ) { - $this->error(); // chyba 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // ověříme, že akce je 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... přidáme políčka formuláře ... - - $form->setDefaults($this->record); // nastavení výchozích hodnot - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // aktualizace záznamu - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -V metodě *action*, která se spouští hned na začátku [životního cyklu presenteru|application:presenters#zivotni-cyklus-presenteru], ověříme existenci záznamu a oprávnění uživatele jej editovat. - -Záznam si uložíme do property `$record`, abychom jej měli k dispozici v metodě `createComponentRecordForm()` kvůli nastavení výchozích hodnot, a `recordFormSucceeded()` kvůli ID. Alternativním řešením by bylo nastavit výchozí hodnoty přímo v `actionEdit()` a hodnotu ID, která je součástí URL, získat pomocí `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // oveření existence a kontrola oprávnění - ) { - $this->error(); - } - - // nastavení výchozích hodnot formuláře - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Nicméně, a to by mělo být **nejdůležitejším poznatkem celého kódu**, musíme se při tvorbě formuláře ujistit, že akce je skutečně `edit`. Protože jinak by ověření v metodě `actionEdit()` vůbec neproběhlo! - - -Stejný formulář pro přidání i editaci -------------------------------------- - -A nyní oba presentery spojíme do jednoho. Buď bychom mohli v metodě `createComponentRecordForm()` rozlišit, o kterou akci jde a podle toho formulář nakonfigurovat, nebo to můžeme nechat přímo na action-metodách a zbavit se podmínky: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $facade; - - public function __construct(Facade $facade) - { - $this->facade = $facade; - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // oveření existence záznamu - || !$this->facade->isEditAllowed(/*...*/) // kontrola oprávnění - ) { - $this->error(); // chyba 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // nastavení výchozích hodnot - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // ověříme, že akce je 'add' nebo 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... přidáme políčka formuláře ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // přidání záznamu do databáze - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // aktualizace záznamu - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/cs/dynamic-snippets.texy b/best-practices/cs/dynamic-snippets.texy deleted file mode 100644 index 3749cc686a..0000000000 --- a/best-practices/cs/dynamic-snippets.texy +++ /dev/null @@ -1,176 +0,0 @@ -Dynamické snippety -****************** - -Poměrně často při vývoji aplikací vyvstává potřeba provádět AJAXové operace například nad jednotlivými řádky tabulky či položkami seznamu. Pro příklad si můžeme zvolit výpis článků, přičemž u každého z nich umožníme přihlášenému uživateli zvolit hodnocení "líbí/nelíbí". Kód presenteru a odpovídající šablony bez AJAXu bude vypadat přibližně následovně (uvádím nejdůležitější výseky, kód počítá s existencí služby pro značení si hodnocení a získáním kolekce článků - konkrétní implementace není pro účely tohoto návodu důležitá): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Šablona: - -```latte - -``` - - -Ajaxizace -========= - -Pojďme nyní tuto jednoduchou aplikaci vybavit AJAXem. Změna hodnocení článku není natolik důležitá, aby muselo dojít k přesměrování, a proto by ideálně měla probíhat AJAXem na pozadí. Využijeme [obslužného skriptu z doplňků |https://componette.org/vojtech-dobes/nette.ajax.js/] s obvyklou konvencí, že AJAXové odkazy mají CSS třídu `ajax`. - -Nicméně jak na to konkrétně? Nette nabízí 2 cesty: cestu tzv. dynamických snippetů a cestu komponent. Obě dvě mají svá pro a proti, a proto si je ukážeme jednu po druhé. - - -Cesta dynamických snippetů -========================== - -Dynamický snippet znamená v terminologii Latte specifický případ užití makra `{snippet}`, kdy je v názvu snippetu použita proměnná. Takový snippet se nemůže v šabloně nalézat jen tak kdekoliv - musí být obalen statickým snippetem, tj. obyčejným, nebo uvnitř `{snippetArea}`. Naši šablonu bychom mohli upravit následovně. - - -```latte -{snippet articlesContainer} - -{/snippet} -``` - -Každý článek nyní definuje jeden snippet, který má v názvu ID článku. Všechny tyto snippety jsou pak dohromady zabalené jedním snippetem s názvem `articlesContainer`. Pokud bychom tento obalující snippet opomněli, Latte nás na to upozorní výjimkou. - -Zbývá nám doplnit do presenteru překreslení - stačí překreslit statickou obálku. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- není potřeba - } else { - $this->redirect('this'); - } -} -``` - -Nápodobně upravíme i sesterskou metodu `handleUnlike()`, a AJAX je funkční! - -Řešení má však jednu stinnou stránku. Pokud bychom více zkoumali, jak AJAXový požadavek probíhá, zjistíme, že ačkoliv navenek se aplikace tváří úsporně (vrátí pouze jeden jediný snippet pro daný článek), ve skutečnosti na serveru vykreslila snippety všechny. Kýžený snippet nám umístila do payloadu, a ostatní zahodila (zcela zbytečně je tedy také získala z databáze). - -Abychom tento proces zoptimalizovali, budeme muset zasáhnout tam, kde si do šablony předáváme kolekci `$articles` (dejme tomu v metodě `renderDefault()`). Využijeme faktu, že zpracování signálů probíhá před metodami `render`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->connection->table('articles')->get($articleId) - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); - } -} -``` - -Nyní se při zpracování signálu do šablony předá místo kolekce se všemi články jen pole s jediným článkem - tím, který chceme vykreslit a odeslat v payloadu do prohlížeče. `{foreach}` tedy proběhne jen jednou a žádné snippety navíc se nevykreslí. - - -Cesta komponent -=============== - -Úplně jiný způsob řešení se dynamickým snippetům vyhne. Trik spočívá v přenesení celé logiky do zvláštní komponenty - o zadávání hodnocení se nám od teď nebude starat presenter, ale vyhrazená `LikeControl`. Třída bude vypadat následovně (kromě toho bude obsahovat i metody `render`, `handleUnlike` atd.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - private $article; - - public function __construct($article) - { - $this->article = $article; - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Šablona komponenty: - -```latte -{snippet} - {if !$article->liked} - to se mi líbí - {else} - už se mi to nelíbí - {/if} -{/snippet} -``` - -Samozřejmě se nám změní šablona view a do presenteru budeme muset doplnit továrničku. Protože komponentu vytvoříme tolikrát, kolik z databáze získáme článků, využijeme k jejímu "rozmnožení" třídu [application:Multiplier]. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->connection->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Šablona view se zmenší na nezbytné minimum (a zcela prosté snippetů!): - -```latte -
    -

    {$article->title}

    -
    {$article->content}
    - {control "likeControl-$article->id"} -
    -``` - -Máme téměř hotovo: aplikace nyní bude fungovat AJAXově. I zde nás čeká aplikaci optimalizovat, protože vzhledem k použití Nette Database se při zpracování signálu zbytečně načtou všechny články z databáze namísto jednoho. Výhodou však je, že nedojde k jejich vykreslování, protože se vyrenderuje skutečně jen naše komponenta. - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/cs/editors-and-tools.texy b/best-practices/cs/editors-and-tools.texy deleted file mode 100644 index 6fc1f2a67e..0000000000 --- a/best-practices/cs/editors-and-tools.texy +++ /dev/null @@ -1,86 +0,0 @@ -Editory & nástroje -****************** - -.[perex] -Můžete být zdatný programátor, ale teprve s dobrými nástroji se z vás stane mistr. V této kapitole najdete tipy na důležité nástroje, editory a pluginy. - - -IDE editor -========== - -Rozhodně doporučujeme pro vývoj používat plnohodnotné IDE, jako je třeba PhpStorm, NetBeans, VS Code, a nikoliv jen textový editor s podporou PHP. Rozdíl je opravdu zásadní. Není důvod se spokojit s pouhým editorem, který sice umí obarvovat syntaxi, ale nedosahuje možností špičkového IDE, které přesně napovídá, hlídá chyby, umí refaktorovat kód a spoustu dalšího. Některé IDE jsou placené, jiné dokonce zdarma. - -**NetBeans IDE** má podporu pro Nette, Latte a NEON už vestavěnou. - -**PhpStorm**: nainstalujte si tyto pluginy v `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: najděte v marketplace "Nette Latte + Neon" plugin. - -Také si propojte Tracy s editorem. Při zobrazení chybové stránky pak půjde kliknout na jména souborů a ty se otevřou v editoru s kurzorem na příslušné řádce. Přečtěte si, [jak systém nakonfigurovat|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan je nástroj, který odhalí logické chyby v kódu dřív, než jej spustíte. - -Nainstalujeme jej pomocí Composeru: - -```bash -composer require --dev phpstan/phpstan-nette -``` - -Vytvoříme v projektu konfigurační soubor `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -A následně jej necháme zanalyzovat třídy ve složce `app/`: - -```bash -vendor/bin/phpstan analyse app -``` - -Vyčerpávající dokumentaci najdete přímo na [stránkách PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] zkontroluje a případně opraví některé z formálních chyb ve vašich zdrojových kódech: - -- odstraňuje [BOM |nette:glossary#bom] -- kontroluje validitu [Latte |latte:] šablon -- kontroluje validitu souborů `.neon`, `.php` a `.json` -- kontroluje výskyt [kontrolních znaků |nette:glossary#kontrolní znaky] -- kontroluje, zda je soubor kódován v UTF-8 -- kontroluje chybně zapsané `/* @anotace */` (chybí hvězdička) -- odstraňuje ukončovací `?>` u PHP souborů -- odstraňuje pravostranné mezery a zbytečné řádky na konci souboru -- normalizuje oddělovače řádků na systémové (pokud uvedete volbu `-l`) - - -Composer -======== - -[Composer] je nástroj na správu závislostí v PHP. Dovoluje nám deklarovat libovolně složité závislosti jednotlivých knihoven a pak je za nás nainstaluje do našeho projektu. - - -Requirements Checker -==================== - -Šlo o nástroj, který testoval běhové prostředí serveru a informoval, zda (a do jaké míry) je možné framework používat. V současnosti je Nette možné používat na každém serveru, který má minimální požadovanou verzi PHP. - -{{sitename: Best Practices}} diff --git a/best-practices/cs/form-reuse.texy b/best-practices/cs/form-reuse.texy deleted file mode 100644 index 31b149d086..0000000000 --- a/best-practices/cs/form-reuse.texy +++ /dev/null @@ -1,185 +0,0 @@ -Znovupoužití formulářů na více místech -************************************** - -.[perex] -Jak použít stejný formulář na více místech a neduplikovat kód? To je v Nette opravdu snadné a máte na výběr víc způsobů. - - -Továrna na formulář -=================== - -Vytvoříme si třídu, která umí formulář vyrobit. Takové třídě se říká továrna. V místě, kde budeme chtít formulář použít (např. v presenteru), si továrnu [vyžádáme jako závislosti|dependency-injection:passing-dependencies]. - -Součástí továrny je i kód, který po úspěšném odeslaní formuláře předá data k dalšímu zpracování. Obvykle do modelové vrstvy. Zároveň zkontroluje, zda vše proběhlo v pořádku, a případné chyby [předá zpět |forms:validation#Chyby při zpracování] do formuláře. Model v následujícím příkladu reprezentuje třída `Facade`: - -```php -use Nette\Application\UI\Form; - -class EditFormFactory -{ - private $facade; - - public function __construct(Facade $facade) - { - $this->facade = $facade; - } - - public function create(/* parametry */): Form - { - $form = new Form; - - // přidáme prvky do formuláře - - $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $values): void - { - try { - // zpracování formuláře - $this->facade->process($values); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Továrna může být samozřejmě parametrická, tj. může přijímat parametery, které ovlivní podobu vytvářeného formuláře. - -Nyní si ukážeme předání továrny do presenteru. Nejprve ji zapíšeme do konfiguračního souboru: - -```neon -services: - - EditFormFactory -``` - -A poté vyžádáme v presenteru. Tam také následuje další krok zpracování odeslaného formuláře a tím je přesměrování na další stránku: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - private $formFactory; - - public function __construct(EditFormFactory $formFactory) - { - $this->formFactory = $formFactory; - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - - $form->onSuccess[] = function (Form $form) { - $this->redirect('this'); - }; - - return $form; - } -} -``` - -Tím, že přesměrování řeší až handler v presenteru, lze komponentu použít na více místech a na každém přesměrovat jinam. - - -Komponenta s formulářem -======================= - -Další možností je vytvořit novou [komponentu|application:components], jejíž součástí bude formulář. To nám dává možnost například renderovat formulář specifickým způsobem, neboť součástí komponenty je i šablona. -Nebo lze využít signály pro AJAXovou komunikaci a donačítání informací do formuláře, například pro napovídání, atd. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public $onSave; - - private $facade; - - public function __construct(Facade $facade) - { - $this->facade = $facade; - } - - protected function createComponentForm(): Form - { - $form = new Form; - - // přidáme prvky do formuláře - - $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $values): void - { - try { - // zpracování formuláře - $this->facade->process($values); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // vyvolání události - $this->onSave($this, $values); - } -} -``` - -Ještě vytvoříme továrnu, která bude tuto komponentu vyrábět. Stačí [zapsat její rozhraní|application:components#Komponenty se závislostmi]: - - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -A přidat do konfiguračního souboru: - -```neon -services: - - EditControlFactory -``` - -A nyní už můžeme továrnu vyžádat a použít v presenteru: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - private $controlFactory; - - public function __construct(EditControlFactory $controlFactory) - { - $this->controlFactory = $controlFactory; - } - - protected function createComponentEditForm(): Form - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // nebo přesměrujeme na výsledek editace, např.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/cs/inject-method-attribute.texy b/best-practices/cs/inject-method-attribute.texy deleted file mode 100644 index 294e169d81..0000000000 --- a/best-practices/cs/inject-method-attribute.texy +++ /dev/null @@ -1,101 +0,0 @@ -Metody a atributy inject -************************ - -.[perex] -Na konkrétních případech si přiblížíme možnosti předávání závislostí do presenterů a vysvětlíme si metody a atributy/anotace `inject`. - - -Metody `inject*()` -================== - -V presenterech, stejně jako v každém jiném kódu, je preferovaný způsob předávání závislostí pomocí [konstruktoru |dependency-injection:passing-dependencies#Předávání konstruktorem]. Pokud však presenter dědí od společného předka (např. `BasePresenter`), je lepší v tomto předkovi použít metody `inject*()`. Jedná se o zvláštní případ setteru, kdy metoda začíná prefixem `inject`. Jeho použitím si totiž ponecháme konstruktor volný pro potomky: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - /** @var Foo */ - private $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - /** @var Bar */ - private $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Základní rozdíl od setteru je ten, že Nette DI takto pojmenované metody v presenterech automaticky volá hned po vytvoření instance a předá jim všechny požadované závislosti. Metod `inject*()` může presenter obsahovat více a každá může mít libovolný počet parametrů. - -Pokud bychom závislosti předávali předkům skrze jejich konstruktory, museli bychom ve všech potomcích získávat i jejich závislosti a předávat je do `parent::__construct()`, což komplikuje kód: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - /** @var Foo */ - private $foo; - - public function __construct(Foo $foo) - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - /** @var Bar */ - private $bar; - - public function __construct(Foo $foo, Bar $bar) - { - parent::__construct($foo); // tohle je komplikace - $this->bar = $bar; - } -} -``` - -Metody `inject*()` se hodí také v případech, kdy je presenter [složen z trait |presenter-traits] a každá z nich si vyžádá vlastní závislost. - -Je také možné použít anotaci `@inject`, je však třeba mít na paměti, že dojde k porušení zapouzdření. - - -Anotace `inject` -================ - -Jedná se o automatické předávání závislosti do veřejné členské proměnné presenteru, která je označená anotací `@inject` v dokumentačním komentáři. Typ závislosti je možné uvést také v dokumentačním komentáři, pokud používáte PHP nižší než 7.4. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - /** @var Cache @inject */ - public $cache; -} -``` - -Od PHP 8.0 lze proměnnou označit pomocí atributu `Inject`: - -```php -use Nette\DI\Attributes\Inject; - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Nette DI opět takto anotovaným proměnným v presenteru automaticky předá závislosti hned po vytvoření instance. - -Tento způsob má stejné nedostatky, jako předávání závislosti do veřejné proměnné. V presenterech se používá proto, že nekomplikuje kód a vyžaduje jen minimum psaní. - - -{{sitename: Best Practices}} diff --git a/best-practices/cs/pagination.texy b/best-practices/cs/pagination.texy deleted file mode 100644 index 2eb9e155ac..0000000000 --- a/best-practices/cs/pagination.texy +++ /dev/null @@ -1,302 +0,0 @@ -Stránkování výsledků databáze -***************************** - -.[perex] -Při tvorbě webových aplikací se velmi často setkáte s požadavkem na omezení počtu vypsaných položek na stránce. - -Vyjdeme ze stavu, kdy vypisujeme všechna data bez stránkování. Pro výběr dat z databáze máme třídu ArticleRepository, která kromě konstruktoru obsahuje metodu `findPublishedArticles`, která vrací všechny publikované články seřazené sestupně podle data publikace. - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - use Nette\SmartObject; - - /** @var Nette\Database\Connection */ - private $database; - - public function __construct(Nette\Database\Connection $database) - { - $this->database = $database; - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime - ); - } -} -``` - -V presenteru si pak injectujeme modelovou třídu a v render metodě si vyžádáme publikované články, které předáme do šablony: - -```php -namespace App\Presenters; - -use Nette; -use App\Model\ArticleRepository; - -class HomepagePresenter extends Nette\Application\UI\Presenter -{ - /** @var ArticleRepository */ - private $articleRepository; - - public function __construct(ArticleRepository $articleRepository) - { - $this->articleRepository = $articleRepository; - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -V šabloně se pak postaráme o výpis článků: - -```latte -{block content} -

    Články

    - -
    - {foreach $articles as $article} -

    {$article->title}

    -

    {$article->content}

    - {/foreach} -
    -``` - - -Tímto způsobem umíme vypsat všechny články, což však začne působit problémy v momentě, kdy počet článků vzroste. V tom okamžiku se příjde vhod implementace stránkovacího mechanismu. - -Ten zajistí, že se všechny články rozdělí do několika stránek a my zobrazíme jen články jedné aktuální stránky. Celkový počet stránek a rozdělení článků si vypočte [utils:Paginator] sám podle toho, kolik článků celkem máme a kolik článků na stránku chceme zobrazit. - -V prvním kroku si upravíme metodu pro získání článků ve třídě repositáře tak, aby nám uměla vracet jen články pro jednu stránku. Také přidáme metodu pro zjištění celkového počtu článku v databázi, kterou budeme potřebovat pro nastavení Paginatoru: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - use Nette\SmartObject; - - /** @var Nette\Database\Connection */ - private $database; - - public function __construct(Nette\Database\Connection $database) - { - $this->database = $database; - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset - ); - } - - /** - * Vrací celkový počet publikovaných článků - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Následně se pustíme do úprav presenteru. Do render metody budeme předávat číslo aktuálně zobrazené stránky. Pro případ, kdy nebude toto číslo součástí URL, nastavíme výchozí hodnotu první stránky. - -Dále také render metodu rozšíříme o získání instance Paginatoru, jeho nastavení a výběru správných článků pro zobrazení v šabloně. HomepagePresenter bude po úpravách vypadat takto: - -```php -namespace App\Presenters; - -use Nette; -use App\Model\ArticleRepository; - -class HomepagePresenter extends Nette\Application\UI\Presenter -{ - /** @var ArticleRepository */ - private $articleRepository; - - public function __construct(ArticleRepository $articleRepository) - { - $this->articleRepository = $articleRepository; - } - - public function renderDefault(int $page = 1): void - { - // Zjistíme si celkový počet publikovaných článků - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Vyrobíme si instanci Paginatoru a nastavíme jej - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // celkový počet článků - $paginator->setItemsPerPage(10); // počet položek na stránce - $paginator->setPage($page); // číslo aktuální stránky - - // Z databáze si vytáhneme omezenou množinu článků podle výpočtu Paginatoru - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // kterou předáme do šablony - $this->template->articles = $articles; - // a také samotný Paginator pro zobrazení možností stránkování - $this->template->paginator = $paginator; - } -} -``` - -Šablona nám už nyní iteruje jen nad články jedné stránky, stačí nám přidat stránkovací odkazy: - -```latte -{block content} -

    Články

    - -
    - {foreach $articles as $article} -

    {$article->title}

    -

    {$article->content}

    - {/foreach} -
    - - -``` - - -Takto jsme doplnili stránku o možnost stránkování pomocí Paginatoru. V případě, kdy namísto [Nette Database Core |database:core] jako databázovou vrstvu použijeme [Nette Database Explorer |database:explorer], jsme schopni implementovat stránkování i bez použití Paginatoru. Třída `Nette\Database\Table\Selection` totiž obsahuje metodu [page |api:Nette\Database\Table\Selection::_page] s logikou stránkování převzatou z Paginatoru. - -Repozitář bude při tomto způsobu implementace vypadat takto: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - use Nette\SmartObject; - - /** @var Nette\Database\Explorer */ - private $database; - - - public function __construct(Nette\Database\Explorer $database) - { - $this->database = $database; - } - - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -V presenteru nemusíme vytvářet Paginator, použijeme místo něj metodu třídy `Selection`, kterou nám vrací repositář: - -```php -namespace App\Presenters; - -use Nette; -use App\Model\ArticleRepository; - -class HomepagePresenter extends Nette\Application\UI\Presenter -{ - /** @var ArticleRepository */ - private $articleRepository; - - public function __construct(ArticleRepository $articleRepository) - { - $this->articleRepository = $articleRepository; - } - - public function renderDefault(int $page = 1): void - { - // Vytáhneme si publikované články - $articles = $this->articleRepository->findPublishedArticles(); - - // a do šablony pošleme pouze jejich část omezenou podle výpočtu metody page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // a také potřebná data pro zobrazení možností stránkování - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Protože do šablony nyní neposíláme Paginator, upravíme část zobrazující stránkovací odkazy: - -```latte -{block content} -

    Články

    - -
    - {foreach $articles as $article} -

    {$article->title}

    -

    {$article->content}

    - {/foreach} -
    - - -``` - -Tímto způsobem jsme implementovali stránkovací mechanismus bez použití Paginatoru. - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/cs/passing-settings-to-presenters.texy b/best-practices/cs/passing-settings-to-presenters.texy deleted file mode 100644 index 197d4c78ce..0000000000 --- a/best-practices/cs/passing-settings-to-presenters.texy +++ /dev/null @@ -1,51 +0,0 @@ -Předání nastavení do presenterů -******************************* - -.[perex] -Potřebujete do presenterů předávat argumenty, které nejsou objekty (např. informaci, zda běží v debug režimu, cesty k adresářům apod.), a tedy nemohou být předány automaticky pomocí autowiringu? Řešením je zapouzdřit je do objektu `Settings`. - -Služba `Settings` přestavuje velmi snadný a přitom užitečný způsob, jak poskytovat informace o běžící aplikaci presenterům. Její konkrétní podoba záleží čistě na vašich konkrétních potřebách. Příklad: - -```php -namespace App; - -class Settings -{ - public function __construct( - // od PHP 8.1 je možné uvést readonly - public bool $debugMode, - public string $appDir, - // a tak dále - ) {} -} -``` - -Ukázka registrace do konfigurace: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Když bude presenter potřebovat informace poskytované touto službou, jednoduše si o ni řekne v konstruktoru: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` - -{{sitename: Best Practices}} diff --git a/best-practices/cs/presenter-traits.texy b/best-practices/cs/presenter-traits.texy deleted file mode 100644 index 56c6ba8dfd..0000000000 --- a/best-practices/cs/presenter-traits.texy +++ /dev/null @@ -1,50 +0,0 @@ -Skládání presenterů z trait -*************************** - -.[perex] -Pokud potřebujeme ve více presenterech implementovat stejný kód (např. ověření, že je uživatel přihlášen), nabízí se umístit kód do společného předka. Druhou možností je vytvoření jednoúčelových trait. - -Výhoda tohoto řešení je, že každý z presenterů může použít právě ty traity, které skutečně potřebuje, zatímco vícenásobná dědičnost není v PHP možná. - -Tyto traity mohou využívat skutečnosti, že při vytvoření presenteru se postupně zavolají všechny [inject metody|inject-method-attribute#Metody inject]. Jen je nutné dohlédnout na to, aby název každé inject metody byl unikátní. - -Traity mohou navěsit inicializační kód do událostí [onStartup nebo onRender |application:presenters#Události]. - -Příklady: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Presenter pak tyto traity jednoduše použije: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` - - -{{sitename: Best Practices}} diff --git a/best-practices/cs/restore-request.texy b/best-practices/cs/restore-request.texy deleted file mode 100644 index 299f6e05e0..0000000000 --- a/best-practices/cs/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Jak se vrátit k dřívější stránce? -********************************* - -.[perex] -Co když uživatel vyplňuje formulář a vyprší mu přihlášení? Aby o data nepřišel, před přesměrováním na přihlašovací stránku data uložíme do session. V Nette to je úplná hračka. - -Aktuální požadavek lze uložit do session pomocí metody `storeRequest()`, která vrátí jeho identifikátor v podobě krátkého řetězce. Metoda uloží název aktuálního presenteru, view a jeho parametry. -V případě, že byl odeslán i formulář, uloží se také obsah políček (s výjimkou uploadovaných souborů). - -Obnovení požadavku provádí metoda `restoreRequest($key)`, které předáme získaný identifikátor. Ta přesměruje na původní presenter a view. Pokud však uložený požadavek obsahuje odeslání formuláře, na původní presenter přejde metodou `forward()`, formuláři předá dříve vyplněné hodnoty a nechá jej znovu vykreslit. Uživatel tak má možnost formulář opětovně odeslat a žádná data se neztratí. - -Důležité je, že `restoreRequest()` kontroluje, zda nově přihlášený uživatel je tentýž, co formulář původně vyplňoval. Pokud ne, požadavek zahodí a nic neudělá. - -Ukážeme si vše na příkladu. Mějme presenter `AdminPresenter`, ve kterém se editují data a v jehož metodě `startup()` ověřujeme, zda je uživatel přihlášen. Pokud není, přesměrujeme jej na `SignPresenter`. Zároveň si uložíme aktuální požadavek a jeho klíč odešleme do `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -Presenter `SignPresenter` bude krom formuláře pro přihlášení obsahovat i persistentní parametr `$backlink`, do kterého se klíč zapíše. Jelikož parametr je persistentní, bude se přenášet i po odeslání přihlašovacího formuláře. - - -```php -class SignPresenter extends Nette\Application\UI\Presenter -{ - /** @persistent */ - public $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... přidáme políčka formuláře ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... tady uživatele přihlásíme ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Metodě `restoreRequest()` předáme klíč uloženého požadavku a ona přesměruje (nebo přejde) na původní presenter. - -Pokud je ale klíč neplatný (například už v session neexistuje), metoda neudělá nic. Následuje tedy volání `$this->redirect('Admin:')`, které přesměruje na `AdminPresenter`. - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/cs/translations.texy b/best-practices/cs/translations.texy deleted file mode 100644 index 409c441754..0000000000 --- a/best-practices/cs/translations.texy +++ /dev/null @@ -1,84 +0,0 @@ -Překládání formulářů a šablon -***************************** - -.[perex] -Pokud programujete vícejazyčnou aplikaci, budete nejspíš potřebovat stejnou stránku nebo formulář vykreslit v různých jazykových mutacích. - -Nette Framework k tomuto účelu definuje rozhraní pro překlad [api:Nette\Localization\Translator], které má jedinou metodu `translate()`. Ta přijímá zprávu `$message`, což zpravidla bývá řetězec, a libovolné další parametry. Úkolem je vrátit přeložený řetězec. - -V Nette není žádná výchozí implementace, můžete si vybrat podle svých potřeb z několika hotových řešeních, které najdete na [Componette |https://componette.org/search/localization]. V jejich dokumentaci se dozvíte, jak translator konfigurovat. - -K objektu translatoru se potom ve svém kódu dostanete tak, že si jej necháte předat pomocí [dependency injection |dependency-injection:passing-dependencies]. - -Od nette/utils verze 3.2 je název rozhraní `Nette\Localization\Translator`, tedy bez prefixu `I`. - - -Překlad formulářů ------------------ - -[Formuláře|forms:] podporují vypisování textů přes translator. Předáme jim ho pomocí metody `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -Od této chvíle se nejen všechny popisky, ale i všechny chybové hlášky nebo položky select boxů přeloží do jiného jazyka. - -U jednotlivých formulářových prvků je přitom možné nastavit jiný překladač nebo překládání úplně vypnout hodnotou `null`: - -```php -$form->addSelect('carModel', 'Model:', $cars) - ->setTranslator(null); -``` - -U [validačních pravidel|forms:validation] se translatoru předávají i specifické parametry, například u pravidla: - -```php -$form->addPassword('password', 'Heslo:') - ->addCondition($form::MIN_LENGTH, 'Heslo musí mít alespoň %d znaků', 8); -``` - -se volá translator s těmito parametry: - -```php -$translator->translate('Heslo musí mít alespoň %d znaků', 8); -``` - -a tedy může zvolit správný tvar plurálu u slova `znaky` podle počtu. - - -Překlad šablon --------------- - -Šablonám [Latte|latte:] lze nastavit překladač metodou `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Poté lze překladač používat jako filtr `|translate`: - -```latte -{='Košík'|translate} -{$item|translate} -``` - -Je k dispozici podtržítková značka: - -```latte -{_'Košík'} -{_$item} -``` - -Pro překlad části šablony existuje párová značka `{translate}` (od Latte 2.11, dříve se používala značka `{_}`): - -```latte -{translate}Objednávka{/translate} -``` - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/en/@home.texy b/best-practices/en/@home.texy deleted file mode 100644 index f1f0468a28..0000000000 --- a/best-practices/en/@home.texy +++ /dev/null @@ -1,74 +0,0 @@ -Best Practices -************** - -.[perex] -Tutorials, solutions to common problems and best practices for Nette. - - -
    -
    - - -Nette Application ------------------ -- [Inject methods and attributes |inject-method-attribute] -- [Composing presenters from traits |presenter-traits] -- [Passing settings to presenters |passing-settings-to-presenters] -- [How to return to an earlier page |restore-request] -- [Paginating database results |Pagination] -- [Dynamic snippets |dynamic-snippets] - -
    -
    - - -Forms ------ -- [Form for creating and editing record |creating-editing-form] -- [Reuse of forms |form-reuse] -- [Dependent selectboxes |https://blog.nette.org/en/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -
    -
    - - -Other Areas ------------ -- [Translation of forms and templates |translations] -- [How to load configuration file |bootstrap:] - -
    -
    - - -Tools ------ -- [Composer usage tips |composer] -- [How to use Code Checker |code-checker:] -- [Tips on editors & tools |editors-and-tools] - -
    -
    - - -Sample Solution ---------------- -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:getting-started] - -
    -
    - - -Videos ------- -You can find hundreds of recordings from Posobota and videos about Nette under one roof on the "Nette Framework YouTube Channel":https://www.youtube.com/user/NetteFramework. - -
    -
    - -{{sitename: Best Practices}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/en/composer.texy b/best-practices/en/composer.texy deleted file mode 100644 index efee53307c..0000000000 --- a/best-practices/en/composer.texy +++ /dev/null @@ -1,248 +0,0 @@ -Composer Usage Tips -******************* - -
    - -Composer is a tool for dependency management in PHP. It allows you to declare the libraries your project depends on and it will install and update them for you. We will learn: - -- how to install Composer -- use it in new or existing project - -
    - - -Installation -============ - -Composer is an executable `.phar` file that you download and install as follows. - - -Windows -------- - -Use the official installer [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -All you need is 4 commands, which you can copy from [this page |https://getcomposer.org/download/]. - -Further more, by copying into folder that is in system's `PATH`, Composer becomes globally accessible: - -```shell -$ mv ./composer.phar ~/bin/composer # or /usr/local/bin/composer -``` - - -Use in Project -============== - -To start using Composer in your project, all you need is a `composer.json` file. This file describes the dependencies of your project and may contain other metadata as well. The simplest `composer.json` can look like this: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -We're saying here, that our application (or library) depends on package `nette/database` (the package name consists of a vendor name and the project's name) and it wants the version that matches the `^3.0` version constraint. - -So, when we have the `composer.json` file in the project root and we run: - -```shell -composer update -``` - -Composer will download the Nette Database into directory `vendor`. It also creates a `composer.lock` file, which contains information about exactly which library versions it installed. - -Composer generates a `vendor/autoload.php` file. You can simply include this file and start using the classes that those libraries provide without any extra work: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Update to the Latest Version -============================ - -To update all used packages to the latest version according to version constraints defined in `composer.json` use command `composer update`. For example for dependency `"nette/database": "^3.0"` it will install the latest version 3.x.x, but not version 4. - -To update the version constrains in the `composer.json` file to e.g. `"nette/database": "^4.1"`, to enable to install the latest version, use the `composer require nette/database` command. - -To update all used Nette packages, it would be necessary to list them all on the command line, eg: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Which is impractical. Therefore, use a simple script "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff that will do it for you: - -```shell -php composer-frontline.php -``` - - -Creating New Project -==================== - -New Nette project can be created by executing a simple command: - -```shell -composer create-project nette/web-project name-of-the-project -``` - -Instead the `name-of-the-project` you should provide the name of the directory for your project and execute the command. Composer will fetch the `nette/web-project` repository from GitHub, which already contains the `composer.json` file, and right after that install the Nette Framework itself. The only thing which remains is to [check write permissions |nette:troubleshooting#setting-directory-permissions] on directories `temp/` and `log/` and you're ready to go. - - -PHP Version -=========== - -Composer always installs those versions of packages that are compatible with the version of PHP you are currently using. Which, of course, may not be the same version as PHP on your web host. Therefore, it is useful to add information about the PHP version on the host to the `composer.json` file, and then only versions of packages compatible with host will be installed: - -```js -{ - "require": { - ... - }, - "config": { - "platform": { - "php": "7.2" # PHP version on host - } - } -} -``` - - -Packagist.org - Global Repository -================================= - -[Packagist |https://packagist.org] is the main package repository, in which Composer tries to search packages, if not told otherwise. You can also publish your own packages here. - - -What If We Don’t Want the Central Repository --------------------------------------------- - -If we have internal applications or libraries in our company, which cannot be hosted publicly on Packagist, we can create our own repositories for those project. - -More on repositories in [the official documentation |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -A key feature of Composer is that it provides autoloading for all classes it installs, which you start by including a file `vendor/autoload.php`. - -However, it is also possible to use Composer to load other classes outside the folder `vendor`. The first option is to let Composer scan the defined folders and subfolders, find all the classes and include them in the autoloader. To do this, set `autoload > classmap` in `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # includes the src/ folder and its subfolders - ] - } -} -``` - -Subsequently, it is necessary to run the command `composer dumpautoload` with each change and let the autoloading tables regenerate. This is extremely inconvenient, and it is far better to entrust this task to [RobotLoader|robot-loader:], which performs the same activity automatically in the background and much faster. - -The second option is to follow [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Simply saying, it is a system where the namespaces and class names correspond to the directory structure and file names, ie `App\Router\RouterFactory` is located in the file `/path/to/App/Router/RouterFactory.php`. Configuration example: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # the App\ namespace is in the app/ directory - } - } -} -``` - -See [Composer Documentation |https://getcomposer.org/doc/04-schema.md#psr-4] for exactly how to configure this behavior. - - -Testing New Versions -==================== - -You want to test a new development version of a package. How to do it? First, add this pair of options to the `composer.json` file, which will allow you to install development versions of packages, but will only do so if there is no stable version combination that meets the requirements: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -We also recommend deleting the `composer.lock` file, because sometimes Composer incomprehensibly refuses to install and this will solve the problem. - -Let's say the package is `nette/utils` and the new version is 4.0. You install it with the command: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Or you can install a specific version, for example 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -If another package depends on the library and is locked to an older version (e.g. `^3.1`), it is ideal to update the package to work with the new version. -However, if you just want to get around the limitation and force Composer to install the development version and pretend it is an older version (e.g., 3.1.6), you can use the `as` keyword: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Calling Commands -================ - -You can call your own custom commands and scripts through Composer as if they were native Composer commands. Scripts located in the `vendor/bin` folder do not need to specify this folder. - -As an example, we define a script in the `composer.json` file that uses [Nette Tester |tester:] to run tests: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -We then run the tests with `composer tester`. We can call the command even if we are not in the root folder of the project, but in a subdirectory. - - -Send Thanks -=========== - -We will show you a trick that will make open source authors happy. You can easily give a star on GitHub to the libraries that your project uses. Just install the `symfony/thanks` library: - -```shell -composer global require symfony/thanks -``` - -And then run: - -```shell -composer thanks -``` - -Try it! - - -Configuration -============= - -Composer is closely integrated with version control tool [Git |https://git-scm.com]. If you do not use Git, it is necessary to tell it to Composer: - -```shell -composer -g config preferred-install dist -``` - -{{sitename: Best Practices}} diff --git a/best-practices/en/creating-editing-form.texy b/best-practices/en/creating-editing-form.texy deleted file mode 100644 index cd5fa9c313..0000000000 --- a/best-practices/en/creating-editing-form.texy +++ /dev/null @@ -1,212 +0,0 @@ -Form for Creating and Editing a Record -************************************** - -.[perex] -How to properly implement adding and editing a record in Nette, using the same form for both? - -In many cases, the forms for adding and editing a record are the same, differing only by the label on the button. We will show examples of simple presenters where we use the form first to add a record, then to edit it, and finally combine the two solutions. - - -Adding a Record ---------------- - -An example of a presenter used to add a record. We will leave the actual database work to the `Facade` class, whose code is not relevant for the example. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $facade; - - public function __construct(Facade $facade) - { - $this->facade = $facade; - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... add form fields ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // add record to database - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Editing a Record ----------------- - -Now let's see what a presenter used to edit a records would look like: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $facade; - - private $record; - - public function __construct(Facade $facade) - { - $this->facade = $facade; - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // verify the existence of the record - || !$this->facade->isEditAllowed(/*...*/) // check permissions - ) { - $this->error(); // 404 error - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // verify that the action is 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... add form fields ... - - $form->setDefaults($this->record); // set default values - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // update record - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -In the *action* method, which is invoked right at the beginning of the [presenter lifecycle|application:presenters#Life Cycle of Presenter], we verify the existence of the record and the user's permission to edit it. - -We store the record in the `$record` property so that it is available in the `createComponentRecordForm()` method for setting defaults, and `recordFormSucceeded()` for the ID. An alternative solution would be to set the default values directly in `actionEdit()` and the ID value, which is part of the URL, is retrieved using `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // verify existence and check permissions - ) { - $this->error(); - } - - // set default form values - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -However, and this should be **the most important takeaway from all the code**, we need to make sure that the action is indeed `edit` when we create the form. Because otherwise the validation in the `actionEdit()` method wouldn't happen at all! - - -Same Form for Adding and Editing --------------------------------- - -And now we will combine both presenters into one. Either we could distinguish which action is involved in the `createComponentRecordForm()` method and configure the form accordingly, or we can leave it directly to the action-methods and get rid of the condition: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $facade; - - public function __construct(Facade $facade) - { - $this->facade = $facade; - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // verify the existence of the record - || !$this->facade->isEditAllowed(/*...*/) // check permissions - ) { - $this->error(); // 404 error - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // set defaults - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // verify that the action is 'add' or 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... add form fields ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // add record to database - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // update record - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/en/dynamic-snippets.texy b/best-practices/en/dynamic-snippets.texy deleted file mode 100644 index f0f3cdcce9..0000000000 --- a/best-practices/en/dynamic-snippets.texy +++ /dev/null @@ -1,176 +0,0 @@ -Dynamic Snippets -**************** - -Quite often in application development there is a need to perform AJAX operations, for example, in individual rows of a table or list items. As an example, we can choose to list articles, allowing the logged-in user to select a "like/dislike" rating for each of them. The code of the presenter and the corresponding template without AJAX will look something like this (I list the most important snippets, the code assumes the existence of a service for marking up the ratings and getting a collection of articles - the specific implementation is not important for the purposes of this tutorial): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Template: - -```latte - -``` - - -Ajaxization -=========== - -Let's now bring AJAX to this simple application. Changing the rating of an article is not important enough to require a HTTP request with redirect, so ideally it should be done with AJAX in the background. We'll use the [handler script from add-ons |https://componette.org/vojtech-dobes/nette.ajax.js/] with the usual convention that AJAX links have the CSS class `ajax`. - -However, how to do it specifically? Nette offers 2 ways: the dynamic snippet way and the component way. Both of them have their pros and cons, so we will show them one by one. - - -The Dynamic Snippets Way -======================== - -In Latte terminology, a dynamic snippet is a specific use case of the `{snippet}` tag where a variable is used in the snippet name. Such a snippet cannot be found just anywhere in the template - it must be wrapped by a static snippet, i.e. a regular one, or inside a `{snippetArea}`. We could modify our template as follows. - - -```latte -{snippet articlesContainer} -
    -

    {$article->title}

    -
    {$article->content}
    - {snippet article-$article->id} - {if !$article->liked} - I like it - {else} - I don't like it anymore - {/if} - {/snippet} -
    -{/snippet} -``` - -Each article now defines a single snippet, which has an article ID in the title. All these snippets are then wrapped together in a single snippet called `articlesContainer`. If we omit this wrapping snippet, Latte will alert us with an exception. - -All that's left to do is to add redrawing to the presenter - just redraw the static wrapper. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- není potřeba - } else { - $this->redirect('this'); - } -} -``` - -Modify the sister method `handleUnlike()` in the same way, and AJAX is up and running! - -The solution has one downside, however. If we dig more into how the AJAX request works, we find that although the application looks efficient in appearance (it only returns a single snippet for a given article), it actually renders all the snippets on the server. It has placed the desired snippet in our payload, and discarded the others (thus, quite unnecessarily, it also retrieved them from the database). - -To optimize this process, we'll need to take action where we pass the `$articles` collection to the template (say in the `renderDefault()` method). We will take advantage of the fact that signal processing takes place before the `render` methods: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->connection->table('articles')->get($articleId) - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); - } -} -``` - -Now, when the signal is processed, instead of a collection with all articles, only an array with a single article is passed to the template - the one we want to render and send in payload to the browser. Thus, `{foreach}` will be done only once and no extra snippets will be rendered. - - -Component Way -============= - -A completely different solution uses a different approach to avoid dynamic snippets. The trick is to move all the logic into a separate component - from now on, we don't have a presenter to take care of entering the rating, but a dedicated `LikeControl`. The class will look like the following (in addition, it will also contain the `render`, `handleUnlike`, etc. methods): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - private $article; - - public function __construct($article) - { - $this->article = $article; - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Template of component: - -```latte -{snippet} - {if !$article->liked} - I like it - {else} - I don't like it anymore - {/if} -{/snippet} -``` - -Of course we will change the view template and we will have to add a factory to the presenter. Since we will create the component as many times as we receive articles from the database, we will use the [application:Multiplier] class to "multiply" it. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->connection->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -The template view is reduced to the minimum necessary (and completely free of snippets!): - -```latte -
    -

    {$article->title}

    -
    {$article->content}
    - {control "likeControl-$article->id"} -
    -``` - -We are almost done: the application will now work in AJAX. Here too we have to optimize the application, because due to the use of Nette Database, the signal processing will unnecessarily load all articles from the database instead of one. However, the advantage is that there will be no rendering, because only our component is actually rendered. - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/en/editors-and-tools.texy b/best-practices/en/editors-and-tools.texy deleted file mode 100644 index 8241088593..0000000000 --- a/best-practices/en/editors-and-tools.texy +++ /dev/null @@ -1,86 +0,0 @@ -Editors & Tools -*************** - -.[perex] -You can be a skilled programmer, but only with good tools will you become a master. In this chapter you will find tips on important tools, editors and plugins. - - -IDE Editor -========== - -We strongly recommend using a full-featured IDE for development, such as PhpStorm, NetBeans, VS Code, and not just a text editor with PHP support. The difference is really crucial. There is no reason to be satisfied with a classic editor with syntax highlighting, because it doesn't reach the capabilities of a IDE with accurate code suggestion, that can refactor code, and more. Some IDEs are paid, others are free. - -**NetBeans IDE** has built-in support for Nette, Latte and NEON. - -**PhpStorm**: install these plugins in `Settings > Plugins > Marketplace`: -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: find the "Nette Latte + Neon" plugin in the marketplace. - -Also connect Tracy with the editor. When the error page is displayed, you can click on the file names and they will open in the editor with the cursor on the corresponding line. Learn [how to configure the system |tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan is a tool that detects logical errors in your code before you run it. - -Install it via Composer: - -```bash -composer require --dev phpstan/phpstan-nette -``` - -Create a configuration file `phpstan.neon` in the project: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -And then let it analyze the classes in the `app/` folder: - -```bash -vendor/bin/phpstan analyse app -``` - -You can find comprehensive documentation directly at [PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] checks and possibly repairs some of the formal errors in your source code. - -- removes [BOM |nette:glossary#bom] -- checks validity of [Latte |latte:] templates -- checks validity of `.neon`, `.php` and `.json` files -- checks for [control characters |nette:glossary#control characters] -- checks whether the file is encoded in UTF-8 -- controls misspelled `/* @annotations */` (second asterisk missing) -- removes PHP ending tags `?>` in PHP files -- removes trailing whitespace and unnecessary blank lines from the end of a file -- normalizes line endings to system-default (with the `-l` parameter) - - -Composer -======== - -[Composer] is a tool for managing your dependencies in PHP. It allows us to declare library dependencies and it will install them for us, into our project. - - -Requirements Checker -==================== - -It was a tool that tested the server's running environment and informed whether (and to what extent) the framework could be used. Currently, Nette can be used on any server that has the minimum required version of PHP. - -{{sitename: Best Practices}} diff --git a/best-practices/en/form-reuse.texy b/best-practices/en/form-reuse.texy deleted file mode 100644 index f7b3c39358..0000000000 --- a/best-practices/en/form-reuse.texy +++ /dev/null @@ -1,185 +0,0 @@ -Reuse Forms in Multiple Places -****************************** - -.[perex] -How to reuse the same form in multiple places and not duplicate code? This is really easy to do in Nette and you have multiple ways to choose from. - - -Form Factory -============ - -Let's create a class that can create a form. Such a class is called a factory. In the place where we want to use the form (e.g. in the presenter), we request the [factory as dependency|dependency-injection:passing-dependencies]. - -Part of the factory is the code that passes the data for further processing when the form is successfully submitted. Usually to the model layer. It also checks if everything went well, and [passes back |forms:validation#Processing-errors] any errors to the form. The model in the following example is represented by the `Facade` class: - -```php -use Nette\Application\UI\Form; - -class EditFormFactory -{ - private $facade; - - public function __construct(Facade $facade) - { - $this->facade = $facade; - } - - public function create(/* parameters */): Form - { - $form = new Form; - - // add elements to the form - - $form->addSubmit('send', 'Submit'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $values): void - { - try { - // form processing - $this->facade->process($values); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Of course, the factory can be parametric, i.e. it can receive parameters that will affect the appearance of the form being created. - -We will now demonstrate passing the factory to the presenter. First, we write it to the configuration file: - -```neon -services: - - EditFormFactory -``` - -And then request it in the presenter. There also follows the next step of processing the submitted form and that is redirecting to the next page: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - private $formFactory; - - public function __construct(EditFormFactory $formFactory) - { - $this->formFactory = $formFactory; - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - - $form->onSuccess[] = function (Form $form) { - $this->redirect('this'); - }; - - return $form; - } -} -``` - -Since the redirection is handled by the presenter handler, the component can be used in multiple places and redirected elsewhere in each place. - - -Component with Form -=================== - -Another way is to create a new [component|application:components] that contains a form. This gives us the ability to render the form in a specific way, for example, since the component includes a template. -Or we can use signals for AJAX communication and loading information into the form, for example for auto-completion, etc. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public $onSave; - - private $facade; - - public function __construct(Facade $facade) - { - $this->facade = $facade; - } - - protected function createComponentForm(): Form - { - $form = new Form; - - // add elements to the form - - $form->addSubmit('send', 'Submit'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $values): void - { - try { - // form processing - $this->facade->process($values); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // event invocation - $this->onSave($this, $values); - } -} -``` - -Next, we'll create the factory that will produce this component. Just [write its interface|application:components#Components with Dependencies]: - - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -And add to the configuration file: - -```neon -services: - - EditControlFactory -``` - -And now we can require the factory and use it in the presenter: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - private $controlFactory; - - public function __construct(EditControlFactory $controlFactory) - { - $this->controlFactory = $controlFactory; - } - - protected function createComponentEditForm(): Form - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // or redirect to the result of the edit, e.g.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/en/inject-method-attribute.texy b/best-practices/en/inject-method-attribute.texy deleted file mode 100644 index 7a822bec39..0000000000 --- a/best-practices/en/inject-method-attribute.texy +++ /dev/null @@ -1,101 +0,0 @@ -Inject Methods and Attributes -***************************** - -.[perex] -Using specific examples, we will look at the possibilities of passing dependencies to presenters and explain the `inject` methods and attributes/annotations. - - -`inject*()` Methods -=================== - -In presenter, as in any other code, the preferred way of passing dependencies is by using [constructor |dependency-injection:passing-dependencies#Constructor Injection]. However, if the presenter inherits from a common ancestor (e.g. `BasePresenter`), it is better to use the methods of `inject*()` in that ancestor. It is a special case of a setter, where the method starts with a prefix `inject`. This is because we keep the constructor free for descendants: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - /** @var Foo */ - private $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - /** @var Bar */ - private $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -The basic difference from a setter is that Nette DI automatically calls methods named this way in presenters as soon as the instance is created, passing all required dependencies to them. A presenter can contain multiple methods `inject*()` and each method can have any number of parameters. - -If we passed dependencies to ancestors through their constructors, we would have to get their dependencies in all descendants and pass them to `parent::__construct()`, which complicates the code: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - /** @var Foo */ - private $foo; - - public function __construct(Foo $foo) - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - /** @var Bar */ - private $bar; - - public function __construct(Foo $foo, Bar $bar) - { - parent::__construct($foo); // this is a complication - $this->bar = $bar; - } -} -``` - -The `inject*()` methods are also useful in cases where the presenter is [composed of traits |presenter-traits] and each of them requires its own dependency. - -It is also possible to use annotation `@inject`, but it is important to keep in mind that encapsulation breaks. - - -`Inject` Annotations -==================== - -This is an automatic passing of the dependency to the presenter's public member variable, which is annotated with `@inject` in the documentation comment. The type can also be specified in the documentation comment if you are using PHP lower than 7.4. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - /** @var Cache @inject */ - public $cache; -} -``` - -Since PHP 8.0, a property can be marked with an attribute `Inject`: - -```php -use Nette\DI\Attributes\Inject; - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Again, Nette DI will automatically pass dependencies to properties annotated in this way in the presenter as soon as the instance is created. - -This method has the same shortcomings as passing dependencies to a public property. It is used in presenter because it does not complicate the code and requires only a minimum of typing. - - -{{sitename: Best Practices}} diff --git a/best-practices/en/pagination.texy b/best-practices/en/pagination.texy deleted file mode 100644 index 28dae6dbaa..0000000000 --- a/best-practices/en/pagination.texy +++ /dev/null @@ -1,304 +0,0 @@ -Paginating Database Results -*************************** - -.[perex] -When developing web applications, you often meet with the requirement to print out a restricted number of records on a page. - -We come out of the state when we list all the data without paging. To select data from the database, we have the ArticleRepository class, which contains the constructor and the `findPublishedArticles` method, which returns all published articles sorted in descending order of publication date. - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - use Nette\SmartObject; - - /** @var Nette\Database\Connection */ - private $database; - - - public function __construct(Nette\Database\Connection $database) - { - $this->database = $database; - } - - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime - ); - } -} -``` - -In the Presenter we then inject the model class and in the render method we will ask for the published articles that we pass to the template: - -```php -namespace App\Presenters; - -use Nette; -use App\Model\ArticleRepository; - -class HomepagePresenter extends Nette\Application\UI\Presenter -{ - /** @var ArticleRepository */ - private $articleRepository; - - public function __construct(ArticleRepository $articleRepository) - { - $this->articleRepository = $articleRepository; - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -In the template, we will take care of rendering an articles list: - -```latte -{block content} -

    Articles

    - -
    - {foreach $articles as $article} -

    {$article->title}

    -

    {$article->content}

    - {/foreach} -
    -``` - - -In this way, we can write all articles, but this will cause problems when the number of articles grows. At that point, it will be useful to implement the paging mechanism. - -This will ensure that all articles are split into several pages and we will only show the articles of one current page. The total number of pages and the distribution of the articles is calculated by [utils:Paginator] itself, depending on how many articles we have in total and how many articles we want to display on the page. - -In the first step, we will modify the method for getting articles in the repository class to return only single-page articles. We will also add a new method to get the total number of articles in the database, which we will need to set a Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - use Nette\SmartObject; - - /** @var Nette\Database\Connection */ - private $database; - - public function __construct(Nette\Database\Connection $database) - { - $this->database = $database; - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset - ); - } - - /** - * Returns the total number of published articles - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -The next step is to edit the presenter. We will forward the number of the currently displayed page to the render method. In the case that this number is not part of the URL, we need to set the default value to the first page. - -We also expand the render method to get the Paginator instance, setting it up, and selecting the correct articles to display in the template. HomepagePresenter will look like this: - -```php -namespace App\Presenters; - -use Nette; -use App\Model\ArticleRepository; - -class HomepagePresenter extends Nette\Application\UI\Presenter -{ - /** @var ArticleRepository */ - private $articleRepository; - - public function __construct(ArticleRepository $articleRepository) - { - $this->articleRepository = $articleRepository; - } - - public function renderDefault(int $page = 1): void - { - // We'll find the total number of published articles - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // We'll make the Paginator instance and set it up - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // total articles count - $paginator->setItemsPerPage(10); // items per page - $paginator->setPage($page); // actual page number - - // We'll find a limited set of articles from the database based on Paginator's calculations - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // which we pass to the template - $this->template->articles = $articles; - // and also Paginator itself to display paging options - $this->template->paginator = $paginator; - } -} -``` - -The template already iterates over articles in one page, just add paging links: - -```latte -{block content} -

    Articles

    - -
    - {foreach $articles as $article} -

    {$article->title}

    -

    {$article->content}

    - {/foreach} -
    - - -``` - - -This is how we've added pagination using Paginator. If [Nette Database Explorer |database:explorer] is used instead of [Nette Database Core |database:core] as a database layer, we are able to implement paging even without Paginator. The `Nette\Database\Table\Selection` class contains the [page |api:Nette\Database\Table\Selection::_ page] method with pagination logic taken from the Paginator. - -The repository will look like this: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - use Nette\SmartObject; - - /** @var Nette\Database\Explorer */ - private $database; - - - public function __construct(Nette\Database\Explorer $database) - { - $this->database = $database; - } - - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -We do not have to create Paginator in the Presenter, instead we will use the method of `Selection` object returned by the repository: - -```php -namespace App\Presenters; - -use Nette; -use App\Model\ArticleRepository; - -class HomepagePresenter extends Nette\Application\UI\Presenter -{ - /** @var ArticleRepository */ - private $articleRepository; - - public function __construct(ArticleRepository $articleRepository) - { - $this->articleRepository = $articleRepository; - } - - public function renderDefault(int $page = 1): void - { - // We'll find published articles - $articles = $this->articleRepository->findPublishedArticles(); - - // and their part limited by page method calculation we'll pass to the template - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // and the necessary data to display paging options as well - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Because we do not use Paginator, we need to edit the section showing the paging links: - -```latte -{block content} -

    Articles

    - -
    - {foreach $articles as $article} -

    {$article->title}

    -

    {$article->content}

    - {/foreach} -
    - - -``` - -In this way, we implemented a paging mechanism without using a Paginator. - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/en/passing-settings-to-presenters.texy b/best-practices/en/passing-settings-to-presenters.texy deleted file mode 100644 index 08a6b619b8..0000000000 --- a/best-practices/en/passing-settings-to-presenters.texy +++ /dev/null @@ -1,51 +0,0 @@ -Passing Settings to Presenters -****************************** - -.[perex] -Do you need to pass arguments to presenters that are not objects (e.g. information about whether it is running in debug mode, directory paths, etc.) and thus cannot be passed automatically by autowiring? The solution is to encapsulate them in a `Settings` object. - -The `Settings` service is a very easy yet useful way to provide information about a running application to presenters. Its specific form depends entirely on your particular needs. Example: - -```php -namespace App; - -class Settings -{ - public function __construct( - // since PHP 8.1 it is possible to specify readonly - public bool $debugMode, - public string $appDir, - // and so on - ) {} -} -``` - -Example of registration to the configuration: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -When the presenter needs the information provided by this service, he simply asks for it in the constructor: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` - -{{sitename: Best Practices}} diff --git a/best-practices/en/presenter-traits.texy b/best-practices/en/presenter-traits.texy deleted file mode 100644 index 15edbdb7bd..0000000000 --- a/best-practices/en/presenter-traits.texy +++ /dev/null @@ -1,50 +0,0 @@ -Composing Presenters from Traits -******************************** - -.[perex] -If we need to implement the same code in multiple presenters (e.g. verification that the user is logged in), it is tempting to place the code in a common ancestor. The second option is to create single-purpose traits. - -The advantage of this solution is that each presenter can use just the traits it actually needs, while multiple inheritance is not possible in PHP. - -These traits can take advantage of the fact that all [inject methods|inject-method-attribute#inject methods] are called sequentially when the presenter is created. You just need to make sure that the name of each inject method is unique. - -Traits can hang the initialization code into [onStartup or onRender |application:presenters#Events] events. - -Examples: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -The presenter then simply uses these traits: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` - - -{{sitename: Best Practices}} diff --git a/best-practices/en/restore-request.texy b/best-practices/en/restore-request.texy deleted file mode 100644 index 66ba200c34..0000000000 --- a/best-practices/en/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -How to Return to an Earlier Page? -********************************* - -.[perex] -What if a user fills out a form and his login expires? To avoid losing the data, we save the data in the session before redirecting to the login page. In Nette, this is a piece of cake. - -The current request can be stored in the session using the `storeRequest()` method, which returns its identifier as a short string. The method stores the name of the current presenter, the view and its parameters. -If a form was also submitted, the values of the fields (except for the uploaded files) are also saved. - -The request is restored by the `restoreRequest($key)` method, to which we pass the retrieved identifier. This redirects to the original presenter and view. However, if the saved request contains a form submission, it will forward to the original presenter using method `forward()`, pass the previously filled values to the form and let it be redrawn. This allows the user to resubmit the form and no data is lost. - -Importantly, `restoreRequest()` checks that the newly logged in user is the same one who originally filled out the form. If not, it discards the request and does nothing. - -Let's demonstrate everything with an example. Let's have a presenter `AdminPresenter` in which data is being edited and whose method `startup()` checks if the user is logged in. If he is not, we redirect him to `SignPresenter`. At the same time, we save the current request and send its key to `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -The `SignPresenter` presenter will contain a persistent `$backlink` parameter to which the key is written, in addition to the log-in form. Since the parameter is persistent, it will be carried over even after the login form is submitted. - - -```php -class SignPresenter extends Nette\Application\UI\Presenter -{ - /** @persistent */ - public $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... add form fields ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... here we sign the user in ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -We pass the key of the saved request to the `restoreRequest()` method and it redirects (or forwards) to the original presenter. - -However, if the key is invalid (for example, no longer exists in the session), the method does nothing. So the next call is `$this->redirect('Admin:')`, which redirects to `AdminPresenter`. - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/en/translations.texy b/best-practices/en/translations.texy deleted file mode 100644 index e88bbd8c65..0000000000 --- a/best-practices/en/translations.texy +++ /dev/null @@ -1,84 +0,0 @@ -Translation of Forms and Templates -********************************** - -.[perex] -When creating multilingual application, you will probably need to render the same page or form in various languages. - -The Nette Framework defines for this purpose a translation interface [api:Nette\Localization\Translator], which has a single method `translate()`. It receives a `$message`, which is usually a string, and any other parameters. Its job is to return the translated string. - -There is no default implementation in Nette, you can choose from several ready-made solutions according to your needs, which you will find on [Componette |https://componette.org/search/localization]. See their documentation to learn how to configure the translator. - -You can then get translator in your code by passing it using [dependency injection |dependency-injection:passing-dependencies]. - -As of nette/utils version 3.2, the interface name is `Nette\Localization\Translator`, ie without the prefix `I`. - - -Form Translation ----------------- - -[Forms|forms:] support printing text via a translator. You set it using the method `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -From this point on, all labels but also all error messages or select box items get translated into another language. - -For individual form controls, it is still possible to set another translator or completely turn the translation off using the `null` value: - -```php -$form->addSelect('carModel', 'Model:', $cars) - ->setTranslator(null); -``` - -For [validation rules |forms:validation], specific parameters are also passed to the translator, for example for rule: - -```php -$form->addPassword('password', 'Password:') - ->addCondition($form::MIN_LENGTH, 'Password must be at least %d characters long', 8); -``` - -the translator is called with the following parameters: - -```php -$translator->translate('Password must be at least %d characters long', 8); -``` - -and thus can choose the correct plural form of word `characters` according to the number. - - -Template Translation --------------------- - -In [Latte|latte:] templates, you can add translator using method `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Then the translator can be used as filter `|translate`: - -```latte -{='Basket'|translate} -{$item|translate} -``` - -There is also an underscore tag: - -```latte -{_'Basket'} -{_$item} -``` - -There is a `{translate}` pair tag for translating parts of the template (since Latte 2.11, previously the `{_}` tag was used): - -```latte -{translate}Order{/translate} -``` - -{{priority: -1}} -{{sitename: Best Practices}} diff --git a/best-practices/meta.json b/best-practices/meta.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/best-practices/meta.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/bootstrap/cs/@home.texy b/bootstrap/cs/@home.texy deleted file mode 100644 index ae657b5f0d..0000000000 --- a/bootstrap/cs/@home.texy +++ /dev/null @@ -1,101 +0,0 @@ -Jak načíst konfigurační soubor -****************************** - -.[perex] -Jednotlivé součásti Nette nastavujeme pomocí konfiguračních souborů. Ukážeme si, jak tyto soubory načítat. - -.[tip] -Pokud používate celý framework, není potřeba nic dalšího dělat. V projektu máte pro konfigurační soubory předpřipravený adresář `config/` a jejich načítání má na starosti [zavaděč aplikace|application:bootstrap#konfigurace-di-kontejneru]. -Tento článek je pro uživatele, kteří používají jen jednu knihovnu Nette a chtějí využít možnosti konfiguračních souborů. - -Konfigurační soubory se obvykle zapisují ve [formátu NEON|neon:format] a nejlépe se upravují v [editorech s jeho podporou|best-practices:editors-and-tools#ide-editor]. Lze je chápat jako návody, jak **vytvářet a konfigurovat** objekty. Tedy výsledkem načtení konfigurace bude tzv. továrna, což je objekt, který nám na požádání vytvoří další objekty, které chceme používat. Například databázové spojení apod. - -Této továrně se také říká *dependency injection kontejner* (DI container) a pokud by vás zajímaly podrobnosti, přečtěte si kapitolu o [dependency injection |dependency-injection:]. - -Načtení konfigurace a vytvoření kontejneru obstará třída [api:Nette\Bootstrap\Configurator], takže si nejprve nainstalujeme její balíček `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -A vytvoříme instanci třídy `Configurator`. Protože vygenerovaný DI kontejner se bude kešovat na disk, je nutné nastavit cestu k adresáři, kam se bude ukládat: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Na Linuxu nebo macOS nastavte adresáři `temp/` [práva pro zápis |nette:troubleshooting#Nastavení práv adresářů]. - -A dostáváme se k samotným konfiguračním souborům. Ty načteme pomocí `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Pokud chceme přidat více konfiguračních souborů, můžeme funkci `addConfig()` zavolat vícekrát. Pokud se v souborech objeví prvky se stejnými klíči, budou přepsány (nebo v případě polí [sloučeny |dependency-injection:configuration#Slučování]). Později vkládaný soubor má vyšší prioritu než předchozí. - -Posledním krokem je vytvoření DI kontejneru: - -```php -$container = $configurator->createContainer(); -``` - -A ten nám už vytvoří požadované objekty. Pokud například používáte konfiguraci pro [Nette Database|database:configuration], můžete jej požádat o vytvoření databázových spojení: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// nebo -$explorer = $container->getByType(Nette\Database\Explorer::class); -// nebo při vytváření více spojení -$db = $container->getByName('database.main.connection'); -``` - -A nyní už můžete s databází pracovat! - - -Vývojářský vs produkční režim ------------------------------ - -Ve vývojářském režimu se kontejner automaticky aktualizuje při každé změně konfiguračních souborů. V produkčním režimu se vygeneruje jen jednou a změny se nekontrolují. -Vývojářský je tedy zaměřen na maximální pohodlí programátora, produkční na výkon a ostré nasazení. - -Volba režimu se provádí autodetekcí, takže obvykle není potřeba nic konfigurovat nebo ručně přepínat. Režim je vývojářský tehdy, pokud je aplikace spuštěna na localhostu (tj. IP adresa `127.0.0.1` nebo `::1`) a není přitomna proxy (tj. její HTTP hlavička). Jinak běží v produkčním režimu. - -Pokud chceme vývojářský režim povolit i v dalších případech, například programátorům přistupujícím z konkrétní IP adresy, použijeme `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// lze zadat i pole IP adres -``` - -Rozhodně doporučujeme kombinovat IP adresu s cookie. Do cookie `nette-debug` uložíme tajný token, např. `secret1234`, a tímto způsobem aktivujeme vývojářský režim pro programátory přistupující z konkrétní IP adresy a zároveň mající v cookie zmíněný token: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Vývojářský režim můžeme také vypnout úplně, i pro localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parametry ---------- - -V konfiguračním souborech můžete používat také parametry, které se definují [v sekci `parameters`|dependency-injection:configuration#parametry]. - -Lze je také vkládat zvenčí pomocí metody `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Na parametr `projectId` se lze v konfiguraci odkázat zápisem `%projectId%`. - - -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/en/@home.texy b/bootstrap/en/@home.texy deleted file mode 100644 index 44045e6c32..0000000000 --- a/bootstrap/en/@home.texy +++ /dev/null @@ -1,101 +0,0 @@ -How to Load Configuration File -****************************** - -.[perex] -The individual components of Nette are configured using configuration files. We will show how to load these files. - -.[tip] -If you are using the whole framework, there is no need to do anything else. In the project, you have a pre-prepared directory `config/` for the configuration files, and the [application loader|application:bootstrap#DI Container Configuration] is responsible for loading them. -This article is for users who use only one Nette library and want to take advantage of the configuration files. - -Configuration files are usually written in [NEON|neon:format] and are best edited in [editors with support for it|best-practices:editors-and-tools#ide-editor]. They can be thought of as instructions on how to **create and configure** objects. Thus, the result of loading a configuration will be a so-called factory, which is an object that will on demand create other objects you want to use. For example, a database connection, etc. - -This factory is also called a *dependency injection container* (DI container) and if you are interested in the details, read the chapter on [dependency injection |dependency-injection:]. - -Loading the configuration and creating the container is handled by the [api:Nette\Bootstrap\Configurator] class, so we'll install its `nette/bootstrap` package first: - -```shell -composer require nette/bootstrap -``` - -And create an instance of the `Configurator` class. Since the generated DI container will be cached to disk, you need to set the path to the directory where it will be saved: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -On Linux or macOS, set the [write permissions |nette:troubleshooting#Setting directory permissions] for directory `temp/`. - -And we come to the configuration files themselves. These are loaded using `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -If you want to add more configuration files, you can call the `addConfig()` function multiple times. If elements with the same keys appear in the files, they will be overwritten (or [merged |dependency-injection:configuration#Merging] in the case of arrays). A later inserted file has a higher priority than the previous one. - -The last step is to create a DI container: - -```php -$container = $configurator->createContainer(); -``` - -And it will already create the desired objects for us. For example, if you are using the configuration for [Nette Database|database:configuration], you can ask it to create database connections: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// or -$explorer = $container->getByType(Nette\Database\Explorer::class); -// or when creating multiple connections -$db = $container->getByName('database.main.connection'); -``` - -And now you can work with the database! - - -Development vs Production Mode ------------------------------- - -In development mode, the container is automatically updated whenever the configuration files are changed. In production mode, it is generated only once and changes are not checked. -So developer mode is aimed at maximum programmer convenience, production mode is aimed at performance. - -Mode selection is done by autodetection, so there is usually no need to configure or manually switch anything. The mode is development when the application is running on a localhost (i.e., IP address `127.0.0.1` or `::1`) and no proxy (i.e., its HTTP header) is present. Otherwise it runs in production mode. - -If you want to enable development mode in other cases, such as programmers accessing from a specific IP address, use `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// an array of IP addresses can also be specified -``` - -We definitely recommend combining the IP address with a cookie. Store a secret token, e.g. `secret1234`, in the `nette-debug` cookie, and this way you enable development mode for programmers accessing from a specific IP address and also having the token mentioned in the cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -You can also disable developer mode altogether, even for localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parameters ----------- - -You can also use parameters in configuration files, which are defined [in the `parameters` section|dependency-injection:configuration#parameters`]. - -They can also be inserted from outside using the `addDynamicParameters()` method: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -The `projectId` parameter can be referenced in the configuration with the `%projectId%` notation. - - -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/meta.json b/bootstrap/meta.json deleted file mode 100644 index 1af4e4578f..0000000000 --- a/bootstrap/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "3.x", - "repo": "nette/bootstrap", - "composer": "nette/bootstrap" -} diff --git a/caching/cs/@home.texy b/caching/cs/@home.texy deleted file mode 100644 index a3f1bafef5..0000000000 --- a/caching/cs/@home.texy +++ /dev/null @@ -1,451 +0,0 @@ -Cache -***** - -
    - -Cache `[keš]` zrychlí vaši aplikaci tím, že jednou náročně získaná data uloží pro příští použití. Ukážeme si: - -- jak používat cache -- jak změnit úložiště -- jak správně cache invalidovat - -
    - -Používání cache je v Nette velmi snadné, přitom pokrývá i velmi pokročilé potřeby. Je navrženo pro výkon a 100% odolnost. V základu najdete adaptéry pro nejběžnější backendové úložiště. Umožňuje invalidaci založenou na značkách, časovou expiraci, má ochranu proti cache stampede atd. - - -Instalace -========= - -Knihovnu stáhěte a nainstalujete pomocí nástroje [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Základní použití -================ - -Středobodem práce s cache neboli mezipamětí představuje objekt [api:Nette\Caching\Cache]. Vytvoříme si jeho instanci a jako parametr předáme konstruktoru tzv. úložiště. Což je objekt reprezentující místo, kam se budou data fyzicky ukládat (databáze, Memcached, soubory na disku, ...). K úložišti se dostaneme tak, že si jej necháte předat pomocí [dependency injection |dependency-injection:passing-dependencies] s typem `Nette\Caching\Storage`. Vše podstatné se dozvíte v [části Úložiště|#Úložiště]. - -.[warning] -Ve verzi 3.0 mělo rozhraní ještě prefix `I`, takže název byl `Nette\Caching\IStorage`. - -Pro následující ukázky předpokládejme, že máme vytvořený alias `Cache` a v proměnné `$storage` úložiště. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instance of Nette\Caching\Storage -``` - -Cache je vlastně *key–value store*, tedy data čteme a zapisujeme pod klíči stejně jako u asociativních polí. Aplikace se skládají z řady nezávislých částí a pokud všechny budou používat jedno úložiště (představte si jeden adresář na disku), dříve nebo později by došlo ke kolizi klíčů. Nette Framework problém řeší tak, že celý prostor rozděluje na jmenné prostory (podadresáře). Každá část programu pak používá svůj prostor s unikátním názvem a k žádné kolizi již dojít nemůže. - -Název prostoru uvedeme jako druhý parametr konstruktoru třídy Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Nyní můžeme pomocí objektu `$cache` z mezipaměti číst a zapisovat do ní. K obojímu slouží metoda `load()`. Prvním argumentem je klíč a druhým PHP callback, který se zavolá, když klíč není nalezen v cache. Callback hodnotu vygeneruje, vrátí a ta se uloží do cache: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // náročný výpočet - return $computedValue; -}); -``` - -Pokud druhý parametr neuvedeme `$value = $cache->load($key)`, vrátí se `null`, pokud položka v cache není. - -.[tip] -Prima je, že do cache lze ukládat jakékoliv serializovatelné struktury, nemusí to být jen řetězce. A totéž platí dokonce i pro klíče. - -Položku z mezipaměti vymažeme metodou `remove()`: - -```php -$cache->remove($key); -``` - -Uložit položku do mezipaměti lze také metodou `$cache->save($key, $value, array $dependencies = [])`. Preferovaná je nicméně výše uvedený způsob pomocí `load()`. - - -Memoizace -========= - -Memoizace znamená cachování výsledku volání funkce nebo metody, abyste jej mohli použít příště bez vypočítávání stejné věci znovu a znovu. - -Memoizovaně lze volat metody a funkce pomocí `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Funkce `gethostbyaddr()` se tak zavolá pro každý parametr `$ip` jen jednou a příště už se vrátí hodnota z cache. - -Také je možné vytvořit si memoizovaný obal nad metodou nebo funkcí, který lze volat až později: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // poprvé vypočítá -$result = $memoizedFactorial(5); // podruhé z cache -``` - - -Expirace & invalidace -===================== - -S ukládáním do cache je potřeba řešit otázku, kdy se dříve uložená data stanou neplatná. Nette Framework nabízí mechanismus, jak omezit platnost dat nebo je řízeně mazat (v terminologii frameworku „invalidovat“). - -Platnost dat se nastavuje v okamžiku ukládání a to pomocí třetího parametru metody `save()`, např.: - -```php -$cache->save($key, $value, [ - $cache::EXPIRE => '20 minutes', -]); -``` - -Nebo pomocí parametru `$dependencies` předávaného referencí do callbacku metody `load()`, např.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::EXPIRE] = '20 minutes'; - return /* ... */; -}); -``` - -Nebo pomocí 3. parametru v metodě `load()`, např: .{data-version:3.2} - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::EXPIRE => '20 minutes']); -``` - -V dalších ukázkách budeme předpokládat druhou variantu a tedy existenci proměnné `$dependencies`. - - -Expirace --------- - -Nejjednodušší exirace představuje časový limit. Takto uložíme do cache data s platností 20 minut: - -```php -// akceptuje i počet sekund nebo UNIX timestamp -$dependencies[Cache::EXPIRE] = '20 minutes'; -``` - -Pokud bychom chtěli prodloužit dobu platnosti s každým čtením, lze toho docílit následovně, ale pozor, režie cache tím vzroste: - -```php -$dependencies[Cache::SLIDING] = true; -``` - -Šikovná je možnost nechat data vyexpirovat v okamžiku, kdy se změní soubor či některý z více souborů. Toho lze využít třeba při ukládání dat vzniklých zpracováním těchto souborů do cache. Používejte absolutní cesty. - -```php -$dependencies[Cache::FILES] = '/path/to/data.yaml'; -// nebo -$dependencies[Cache::FILES] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Můžeme nechat položku v cache vyexpirovat ve chvíli, kdy vyexpiruje jiná položka (či některá z více jiných). Což lze využít tehdy, když ukládáme do cache třeba celou HTML stránku a pod jinými klíči její fragmenty. Jakmile se fragment změní, invaliduje se celá stránka. Pokud fragmenty máme uložené pod klíči např. `frag1` a `frag2`, použijeme: - -```php -$dependencies[Cache::ITEMS] = ['frag1', 'frag2']; -``` - -Expiraci lze řídit i pomocí vlastních funkcí nebo statických metod, které vždy při čtení rozhodnou, zda je položka ještě platná. Takto třeba můžeme nechat položku vyexpirovat vždy, když se změní verze PHP. Vytvoříme funkci, která porovná aktuální verzi s parameterem, a při ukládání přidáme mezi závislosti pole ve tvaru `[nazev funkce, ...argumenty]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::CALLBACKS] = [ - ['checkPhpVersion', PHP_VERSION_ID] // expiruj když checkPhpVersion(...) === false -]; -``` - -Všechny kritéria je samozřejmě možné kombinovat. Cache pak vyexpiruje, když alespoň jedno kritérium není splněno. - -```php -$dependencies[Cache::EXPIRE] = '20 minutes'; -$dependencies[Cache::FILES] = '/path/to/data.yaml'; -``` - - -Invalidace pomocí tagů ----------------------- - -Velmi užitečným invalidačním nástrojem jsou tzv. tagy. Každé položce v cache můžeme přiřadit seznam tagů, což jsou libovolné řetězce. Mějme třeba HTML stránku s článkem a komentáři, kterou budeme cachovat. Při ukládání specifikujeme tagy: - -```php -$dependencies[Cache::TAGS] = ["article/$articleId", "comments/$articleId"]; -``` - -Přesuňme se do administrace. Tady najdeme formulář pro editaci článku. Společně s uložením článku do databáze zavoláme příkaz `clean()`, který smaže z cache položky dle tagu: - -```php -$cache->clean([ - $cache::TAGS => ["article/$articleId"], -]); -``` - -Stejně tak v místě přidání nového komentáře (nebo editace komentáře) neopomeneme invalidovat příslušný tag: - -```php -$cache->clean([ - $cache::TAGS => ["comments/$articleId"], -]); -``` - -Čeho jsme tím dosáhli? Že se nám HTML cache bude invalidovat (mazat), kdykoliv se změní článek nebo komentáře. Když se edituje článek s ID = 10, dojde k vynucené invalidaci tagu `article/10` a HTML stránka, která uvedený tag nese, se z cache smaže. Totéž nastane při vložení nového komentáře pod příslušný článek. - -.[note] -Tagy vyžadují tzv. [#Journal]. - - -Invalidace pomocí priority --------------------------- - -Jednotlivým položkám v cache můžeme nastavit prioritu, pomocí které je bude možné mazat, když třeba cache přesáhne určitou velikost: - -```php -$dependencies[Cache::PRIORITY] = 50; -``` - -Smažeme všechny položky s prioritou rovnou nebo menší než 100: - -```php -$cache->clean([ - $cache::PRIORITY => 100, -]); -``` - -.[note] -Priority vyžadují tzv. [#Journal]. - - -Smazání cache -------------- - -Parametr `Cache::ALL` smaže vše: - -```php -$cache->clean([ - $cache::ALL => true, -]); -``` - - -Hromadné čtení -============== - -Pro hromadné čtení a zápisy do cache slouží metoda `bulkLoad()`, které předáme pole klíčů a získáme pole hodnot: - -```php -$values = $cache->bulkLoad($keys); -``` - -Metoda `bulkLoad()` funguje podobně jako `load()` i s druhým parameterm callbackem, kterému se předává klíč generované položky: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // náročný výpočet - return $computedValue; -}); -``` - - -Cachování výstupu -================= - -Velmi elegantně lze zachytávat a cachovat výstup: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // vypisujeme data - - $capture->end(); // uložíme výstup do cache -} -``` - -V případě, že výstup už je v cache uložen, tak ho metoda `capture()` vypíše a vrátí `null`, tedy podmínka se nevykoná. V opačném případě začne výstup zachytávat a vrátí objekt `$capture`, pomocí něhož nakonec vypsaná data uložíme do cache. - -.[note] -Ve verzi 3.0 se metoda jmenovala `$cache->start()`. - - -Cachování v Latte -================= - -Cachování v šablonách [Latte|latte:] je velmi snadné, stačí část šablony obalit značkami `{cache}...{/cache}`. Cache se automaticky invaliduje ve chvíli, kdy se změní zdrojová šablona (včetně případných inkludovaných šablon uvnitř bloku cache). Značky `{cache}` lze vnořovat do sebe a když se vnořený blok zneplatní (například tagem), zneplatní se i blok nadřazený. - -Ve značce je možné uvést klíče, na které se bude cache vázat (zde proměnná `$id`) a nastavit expiraci a [tagy pro zneplatnění |#Invalidace pomocí tagů] - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Všechny položky jsou volitelné, takže nemusíme uvádět ani expiraci, ani tagy, nakonec ani klíče. - -Použití cache lze také podmínit pomocí `if` - obsah se pak bude cachovat pouze bude-li splněna podmínka: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Úložiště -======== - -Úložiště je objekt reprezentující místo, kam se data fyzicky ukládají. Můžeme použít databázi, server Memcached, nebo nejdostupnější úložiště, což jsou soubory na disku. - -|----------------- -| Úložiště | Popis -|----------------- -| [#FileStorage] | výchozí úložiště s ukládáním do souborů na disk -| [#MemcachedStorage] | využívá `Memcached` server -| [#MemoryStorage] | data jsou dočasně v paměti -| [#SQLiteStorage] | data se ukládají do SQLite databáze -| [#DevNullStorage] | data se neukládají, vhodné pro testování - -K objektu úložiště se dostanete tak, že si jej necháte předat pomocí [dependency injection |dependency-injection:passing-dependencies] s typem `Nette\Caching\Storage`. Jako výchozí úložiště poskytuje Nette objekt FileStorage ukládající data do podsložky `cache` v adresáři pro [dočasné soubory|application:bootstrap#dočasné soubory]. - -Změnit úložiště můžete v konfiguraci: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Zapisuje cache do souborů na disku. Úložiště `Nette\Caching\Storages\FileStorage` je velmi dobře optimalizované pro výkon a především zajišťuje plnou atomicitu operací. Co to znamená? Že při použití cache se nemůže stát, že přečteme soubor, který ještě není jiným vláknem kompletně zapsaný, nebo že by vám jej někdo "pod rukama" smazal. Použití cache je tedy zcela bezpečné. - -Toto úložiště má také vestavěnou důležitou funkci, která brání před extrémním nárůstem využití CPU ve chvíli, kdy se cache smaže nebo ještě není zahřátá (tj. vytvořená). Jedná se o prevenci před "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. -Stává se, že v jednu chvíli se sejde větší počet souběžných požadavků, které chtějí z cache stejnou věc (např. výsledek drahého SQL dotazu) a protože v mezipaměti není, začnou všechny procesy vykonávat stejný SQL dotaz. -Vytížení se tak násobí a může se dokonce stát, že žádné vlákno nestihne odpovědět v časovém limitu, cache se nevytvoří a aplikace zkolabuje. -Naštěstí cache v Nette funguje tak, že při více souběžných požadavcích na jednu položku ji generuje pouze první vlákno, ostatní čekají a následně využíjí vygenerovaný výsledek. - -Příklad vytvoření FileStorage: - -```php -// úložištěm bude adresář '/path/to/temp' na disku -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Server [Memcached|https://memcached.org] je vysoce výkonný systém ukládání do distribuované paměti, jehož adaptér je `Nette\Caching\Storages\MemcachedStorage`. V konfiguraci uvedeme IP adresu a port, pokud se liší od standardního 11211. - -.[caution] -Vyžaduje PHP rozšíření `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` je úložiště, která data ukládá do PHP pole, a tedy se s ukončením požadavku ztratí. - - -SQLiteStorage -------------- - -Databáze SQLite a adaptér `Nette\Caching\Storages\SQLiteStorage` nabízí způsob, jak ukládat cache do jediného souboru na disku. V konfiguraci uvedeme cestu k tomuto souboru. - -.[caution] -Vyžaduje PHP rozšíření `pdo` a `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Speciální implementací úložiště je `Nette\Caching\Storages\DevNullStorage`, které ve skutečnosti data neukládá vůbec. Je tak vhodné pro testování, když chceme eliminovat vliv cache. - - -Použití cache v kódu -==================== - -Při používání cache v kódu máme dva způsoby, jak na to. První z nich je ten, že si necháme předat pomocí [dependency injection |dependency-injection:passing-dependencies] úložište a vytvoříme objekt `Cache`: - -```php -use Nette; - -class ClassOne -{ - /** @var Nette\Caching\Cache */ - private $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Druhá možnost je, že si necháme rovnou předat objekt `Cache`: - -```php -class ClassTwo -{ - /** @var Nette\Caching\Cache */ - private $cache; - - public function __construct(Nette\Caching\Cache $cache) - { - $this->cache = $cache; - } -} -``` - -Objekt `Cache` se potom vytvoří přímo v konfiguraci tímto způsobem: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette si tagy a priority ukládá do tzv. journalu. Standardně se k tomu používá SQLite a soubor `journal.s3db` a **vyžadují se PHP rozšíření `pdo` a `pdo_sqlite`.** - -Změnit journal můžete v konfiguraci: - -```neon -services: - cache.journal: MyJournal -``` - - -{{leftbar: nette:@menu-topics}} diff --git a/caching/en/@home.texy b/caching/en/@home.texy deleted file mode 100644 index 0cb09e9a7e..0000000000 --- a/caching/en/@home.texy +++ /dev/null @@ -1,451 +0,0 @@ -Caching -******* - -
    - -Cache accelerates your application by storing data - once hard retrieved - for future use. We will show you: - -- How to use the cache -- How to change the cache storage -- How to properly invalidate the cache - -
    - -Using the cache is very easy in Nette, while it also covers very advanced caching needs. It is designed for performance and 100% durability. Basically, you will find adapters for the most common backend storage. Allows tags-based invalidation, cache stampede protection, time expiration, etc. - - -Installation -============ - -Download and install the package using [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Basic Usage -=========== - -The center of work with the cache is the object [api:Nette\Caching\Cache]. We create its instance and pass the so-called storage to the constructor as a parameter. Which is an object representing the place where the data will be physically stored (database, Memcached, files on disk, ...). You get the storage object by passing it using [dependency injection |dependency-injection:passing-dependencies] with type `Nette\Caching\Storage`. You will find out all the essentials in [section Storage |#Storages]. - -.[warning] -In version 3.0, the interface still had the `I` prefix, so the name was `Nette\Caching\IStorage`. - -For the following examples, suppose we have an alias `Cache` and a storage in the variable `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instance of Nette\Caching\Storage -``` - -The cache is actually a *key–value store*, so we read and write data under keys just like associative arrays. Applications consist of a number of independent parts, and if they all used one storage (for idea: one directory on a disk), sooner or later there would be a key collision. The Nette Framework solves the problem by dividing the entire space into namespaces (subdirectories). Each part of the program then uses its own space with a unique name and no collisions can occur. - -The name of the space is specified as the second parameter of the constructor of the Cache class: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -We can now use object `$cache` to read and write from the cache. The method `load()` is used for both. The first argument is the key and the second is the PHP callback, which is called when the key is not found in the cache. The callback generates a value, returns it and caches it: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // heavy computations - return $computedValue; -}); -``` - -If the second parameter is not specified `$value = $cache->load($key)`, the `null` is returned if the item is not in the cache. - -.[tip] -The great thing is that any serializable structures can be cached, not only strings. And the same applies for keys. - -The item is cleared from the cache using method `remove()`: - -```php -$cache->remove($key); -``` - -You can also cache an item using method `$cache->save($key, $value, array $dependencies = [])`. However, the above method using `load()` is preferred. - - -Memoization -=========== - -Memoization means caching the result of a function or method so you can use it next time instead of calculating the same thing again and again. - -Methods and functions can be called memoized using `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -The function `gethostbyaddr()` is called only once for each parameter `$ip` and the next time the value from the cache will be returned. - -It is also possible to create a memoized wrapper for a method or function that can be called later: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // counts it -$result = $memoizedFactorial(5); // returns it from cache -``` - - -Expiration & Invalidation -========================= - -With caching, it is necessary to address the question that some of the previously saved data will become invalid over time. Nette Framework provides a mechanism, how to limit the validity of data and how to delete them in a controlled way ("to invalidate them", using the framework's terminology). - -The validity of the data is set at the time of saving using the third parameter of the method `save()`, eg: - -```php -$cache->save($key, $value, [ - $cache::EXPIRE => '20 minutes', -]); -``` - -Or using the `$dependencies` parameter passed by reference to the callback in the `load()` method, eg: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::EXPIRE] = '20 minutes'; - return /* ... */; -}); -``` - -Or using the 3rd parameter in the `load()` method, eg: .{data-version:3.2} - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::EXPIRE => '20 minutes']); -``` - -In the following examples, we will assume the second variant and thus the existence of a variable `$dependencies`. - - -Expiration ----------- - -The simplest expiration is the time limit. Here's how to cache data valid for 20 minutes: - -```php -// it also accepts the number of seconds or the UNIX timestamp -$dependencies[Cache::EXPIRE] = '20 minutes'; -``` - -If we want to extend the validity period with each reading, it can be achieved this way, but beware, this will increase the cache overhead: - -```php -$dependencies[Cache::SLIDING] = true; -``` - -The handy option is the ability to let the data expire when a particular file is changed or one of several files. This can be used, for example, for caching data resulting from procession these files. Use absolute paths. - -```php -$dependencies[Cache::FILES] = '/path/to/data.yaml'; -// or -$dependencies[Cache::FILES] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -We can let an item in the cache expired when another item (or one of several others) expires. This can be used when we cache the entire HTML page and fragments of it under other keys. Once the snippet changes, the entire page becomes invalid. If we have fragments stored under keys such as `frag1` and `frag2`, we will use: - -```php -$dependencies[Cache::ITEMS] = ['frag1', 'frag2']; -``` - -Expiration can also be controlled using custom functions or static methods, which always decide when reading whether the item is still valid. For example, we can let the item expire whenever the PHP version changes. We will create a function that compares the current version with the parameter, and when saving we will add an array in the form `[function name, ...arguments]` to the dependencies: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::CALLBACKS] = [ - ['checkPhpVersion', PHP_VERSION_ID] // expire when checkPhpVersion(...) === false -]; -``` - -Of course, all criteria can be combined. The cache then expires when at least one criterion is not met. - -```php -$dependencies[Cache::EXPIRE] = '20 minutes'; -$dependencies[Cache::FILES] = '/path/to/data.yaml'; -``` - - -Invalidation Using Tags ------------------------ - -Tags are a very useful invalidation tool. We can assign a list of tags, which are arbitrary strings, to each item stored in the cache. For example, suppose we have an HTML page with an article and comments, which we want to cache. So we specify tags when saving to cache: - -```php -$dependencies[Cache::TAGS] = ["article/$articleId", "comments/$articleId"]; -``` - -Now, let's move to the administration. Here we have a form for article editing. Together with saving the article to a database, we call the `clean()` command, which will delete cached items by tag: - -```php -$cache->clean([ - $cache::TAGS => ["article/$articleId"], -]); -``` - -Likewise, in the place of adding a new comment (or editing a comment), we will not forget to invalidate the relevant tag: - -```php -$cache->clean([ - $cache::TAGS => ["comments/$articleId"], -]); -``` - -What have we achieved? That our HTML cache will be invalidated (deleted) whenever the article or comments change. When editing an article with ID = 10, the tag `article/10` is forced to be invalidated and the HTML page carrying the tag is deleted from the cache. The same happens when you insert a new comment under the relevant article. - -.[note] -Tags require [#Journal]. - - -Invalidation by Priority ------------------------- - -We can set the priority for individual items in the cache, and it will be possible to delete them in a controlled way when, for example, the cache exceeds a certain size: - -```php -$dependencies[Cache::PRIORITY] = 50; -``` - -Delete all items with a priority equal to or less than 100: - -```php -$cache->clean([ - $cache::PRIORITY => 100, -]); -``` - -.[note] -Priorities require so-called [#Journal]. - - -Clear Cache ------------ - -The `Cache::ALL` parameter clears everything: - -```php -$cache->clean([ - $cache::ALL => true, -]); -``` - - -Bulk Reading -============ - -For bulk reading and writing to cache, the `bulkLoad()` method is used, where we pass an array of keys and obtain an array of values: - -```php -$values = $cache->bulkLoad($keys); -``` - -Method `bulkLoad()` works similarly to `load()` with the second callback parameter, to which the key of the generated item is passed: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // heavy computations - return $computedValue; -}); -``` - - -Output Caching -============== - -The output can be captured and cached very elegantly: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // printing some data - - $capture->end(); // save the output to the cache -} -``` - -In case that the output is already present in the cache, the `capture()` method prints it and returns `null`, so the condition will not be executed. Otherwise, it starts to buffer the output and returns the `$capture` object using which we finally save the data to the cache. - -.[note] -In version 3.0 the method was called `$cache->start()`. - - -Caching in Latte -================ - -Caching in [Latte|latte:] templates is very easy, just wrap part of the template with tags `{cache}...{/cache}`. The cache is automatically invalidated when the source template changes (including any included templates within the `{cache}` tags). Tags `{cache}` can be nested, and when a nested block is invalidated (for example, by a tag), the parent block is also invalidated. - -In the tag it is possible to specify the keys to which the cache will be bound (here the variable `$id`) and set the expiration and [invalidation tags |#Invalidation using Tags] - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -All parameters are optional, so you don't have to specify expiration, tags, or keys. - -The use of the cache can also be conditioned by `if` - the content will then be cached only if the condition is met: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Storages -======== - -A storage is an object that represents where data is physically stored. We can use a database, a Memcached server, or the most available storage, which are files on disk. - -|---------------------- -| Storage | Description -|---------------------- -| [#FileStorage] | default storage with saving to files on disk -| [#MemcachedStorage] | uses the `Memcached` server -| [#MemoryStorage] | data are temporarily in memory -| [#SQLiteStorage] | data is stored in SQLite database -| [#DevNullStorage] | data aren't stored - for testing purposes - -You get the storage object by passing it using [dependency injection |dependency-injection:passing-dependencies] with the `Nette\Caching\Storage` type. By default, Nette provides a FileStorage object that stores data in a subfolder `cache` in the directory for [temporary files |application:bootstrap#Temporary Files] . - -You can change the storage in the configuration: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Writes the cache to files on disk. The storage `Nette\Caching\Storages\FileStorage` is very well optimized for performance and above all ensures full atomicity of operations. What does it mean? That when using the cache, it cannot happen that we read a file that has not yet been completely written by another thread, or that someone would delete it "under your hands". The use of the cache is therefore completely safe. - -This storage also has an important built-in feature that prevents an extreme increase in CPU usage when the cache is cleared or cold (ie not created). This is "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede prevention. -It happens that at one moment there are several concurrent requests that want the same thing from the cache (eg the result of an expensive SQL query) and because it is not cached, all processes start executing the same SQL query. -The processor load is multiplied and it can even happen that no thread can respond within the time limit, the cache is not created and the application crashes. -Fortunately, the cache in Nette works in such a way that when there are multiple concurrent requests for one item, it is generated only by the first thread, the others wait and then use the generated result. - -Example of creating a FileStorage: - -```php -// the storage will be the directory '/path/to/temp' on the disk -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -The server [Memcached |https://memcached.org] is a high-performance distributed storage system whose adapter is `Nette\Caching\Storages\MemcachedStorage`. In the configuration, specify the IP address and port if it differs from the standard 11211. - -.[caution] -Requires PHP extension `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` is a storage that stores data in a PHP array and is thus lost when the request is terminated. - - -SQLiteStorage -------------- - -The SQLite database and adapter `Nette\Caching\Storages\SQLiteStorage` offer a way to cache in a single file on disk. The configuration will specify the path to this file. - -.[caution] -Requires PHP extensions `pdo` and `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -A special implementation of storage is `Nette\Caching\Storages\DevNullStorage`, which does not actually store data at all. It is therefore suitable for testing if we want to eliminate the effect of the cache. - - -Using Cache in Code -=================== - -When using caching in code, you have two ways how to do it. The first is that you get the storage object by passing it using [dependency injection |dependency-injection:passing-dependencies] and then create an object `Cache`: - -```php -use Nette; - -class ClassOne -{ - /** @var Nette\Caching\Cache */ - private $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -The second way is that you get the storage object `Cache`: - -```php -class ClassTwo -{ - /** @var Nette\Caching\Cache */ - private $cache; - - public function __construct(Nette\Caching\Cache $cache) - { - $this->cache = $cache; - } -} -``` - -The `Cache` object is then created directly in the configuration as follows: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette stores tags and priorities in a so-called journal. By default, SQLite and file `journal.s3db` are used for this, and **PHP extensions `pdo` and `pdo_sqlite` are required.** - -You can change the journal in the configuration: - -```neon -services: - cache.journal: MyJournal -``` - - -{{leftbar: nette:@menu-topics}} diff --git a/caching/meta.json b/caching/meta.json deleted file mode 100644 index 8f948e0cbd..0000000000 --- a/caching/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "3.x", - "repo": "nette/caching", - "composer": "nette/caching" -} diff --git a/code-checker/cs/@home.texy b/code-checker/cs/@home.texy deleted file mode 100644 index 1a7cb5a165..0000000000 --- a/code-checker/cs/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Code Checker -************ - -.[perex] -Nástroj [Code Checker |https://github.com/nette/code-checker] zkontroluje a případně opraví některé z formálních chyb ve vašich zdrojových kódech. - - -Instalace -========= - -Code Checker byste neměli přidávat do závislostí, ale instalovat jako projekt. - -```shell -composer create-project nette/code-checker -``` - -Nebo jej nainstalujte globálně pomocí: - -```shell -composer global require nette/code-checker -``` - -a ujistěte se, že váš globální adresář `vendor/bin` je v [proměnné prostředí $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Použití -======= - -``` -Usage: php code-checker [options] - -Options: - -d Folder or file to scan (default: current directory) - -i | --ignore Files to ignore - -f | --fix Fixes files - -l | --eol Convert newline characters - --no-progress Do not show progress dots - --strict-types Checks whether PHP 7.0 directive strict_types is enabled -``` - -Bez parametrů zkontroluje aktuální adresář v read-only režimu, s parametrem `-f` opravuje soubory. - -Než se s ním seznámíte, určitě si soubory nejdřív zazálohujte. - -Pro snadnější spouštění si můžeme vytvořit soubor `code.bat`: - -```shell -php cesta_k_Nette_tools\Code-Checker\code-checker %* -``` - - -Co všechno dělá? -================ - -- odstraňuje [BOM |nette:glossary#bom] -- kontroluje validitu [Latte |latte:] šablon -- kontroluje validitu souborů `.neon`, `.php` a `.json` -- kontroluje výskyt [kontrolních znaků |nette:glossary#kontrolní znaky] -- kontroluje, zda je soubor kódován v UTF-8 -- kontroluje chybně zapsané `/* @anotace */` (chybí hvězdička) -- odstraňuje ukončovací `?>` u PHP souborů -- odstraňuje pravostranné mezery a zbytečné řádky na konci souboru -- normalizuje oddělovače řádků na systémové (pokud uvedete volbu `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/en/@home.texy b/code-checker/en/@home.texy deleted file mode 100644 index b40eb32fc6..0000000000 --- a/code-checker/en/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Code Checker -************ - -.[perex] -The tool called [Code Checker |https://github.com/nette/code-checker] checks and possibly repairs some of the formal errors in your source code. - - -Installation -============ - -Code Checker should be installed as project, don't use it as dependency. - -```shell -composer create-project nette/code-checker -``` - -Or install it globally via: - -```shell -composer global require nette/code-checker -``` - -and make sure your global vendor binaries directory is in [your `$PATH` environment variable|https://getcomposer.org/doc/03-cli.md#global]. - - -Usage -===== - -``` -Usage: php code-checker [options] - -Options: - -d Folder or file to scan (default: current directory) - -i | --ignore Files to ignore - -f | --fix Fixes files - -l | --eol Convert newline characters - --no-progress Do not show progress dots - --strict-types Checks whether PHP 7.0 directive strict_types is enabled -``` - -Without parameters, it checks the current working directory in a read-only mode, with `-f` parameter it fixes files. - -Before you get to know the tool, be sure to backup your files first. - -You can create a batch file, e.g. `code.bat`, for easier execution of Code-Checker under Windows: - -```shell -php path_to\Nette_tools\Code-Checker\code-checker %* -``` - - -What Code-Checker Does? -======================= - -- removes [BOM |nette:glossary#bom] -- checks validity of [Latte |latte:] templates -- checks validity of `.neon`, `.php` and `.json` files -- checks for [control characters |nette:glossary#control characters] -- checks whether the file is encoded in UTF-8 -- controls misspelled `/* @annotations */` (second asterisk missing) -- removes PHP ending tags `?>` in PHP files -- removes trailing whitespace and unnecessary blank lines from the end of a file -- normalizes line endings to system-default (with the `-l` parameter) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/meta.json b/code-checker/meta.json deleted file mode 100644 index 7bfea0012f..0000000000 --- a/code-checker/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "3.x", - "repo": "nette/code-checker", - "composer": "nette/code-checker" -} diff --git a/component-model/cs/@home.texy b/component-model/cs/@home.texy deleted file mode 100644 index 28e3d11fe5..0000000000 --- a/component-model/cs/@home.texy +++ /dev/null @@ -1,74 +0,0 @@ -Komponentový model -****************** - -.[perex] -Důležitým pojmem v Nette je komponenta. Do stránek vkládáme [vizuální interaktivní komponenty |application:components], komponentami jsou i formuláře nebo všechny jejich prvky. Základní dvě třídy, od kterých všechny tyto komponenty dědí, jsou součástí balíčku `nette/component-model` a mají za úkol vytvářet stromovou hierarchii komponent. - - -Component -========= -[api:Nette\ComponentModel\Component] je společným předkem všech komponent. Obsahuje metody `getName()` vracející název kompoenty a metodu `getParent()` vracející jejího rodiče. Obojí lze nastavit metodou `setParent()` - první parametr je rodič a druhý název komponenty. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Vyhledá v hierarchii směrem nahoru objekt požadované třídy nebo rozhraní. Například `$component->lookup(Nette\Application\UI\Presenter::class)` vrací presenter, pokud je k němu, i přes několik úrovní, komponenta připojena. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Vrací tzv. cestu, což je řetězec vzniklý spojením jmen všech komponent na cestě mezi aktuální a hledanou komponentou. Takže např. `$component->lookupPath(Nette\Application\UI\Presenter::class)` vrací jedinečný identifikátor komponenty vůči presenteru. - - -Container -========= -[api:Nette\ComponentModel\Container] je rodičovská komponenta, tj. komponenta obsahující potomky a tvořící tak stromovou strukturu. Disponuje metodami pro snadné přidávání, získávání a odstraňování objektů. Je předkem například formuláře či tříd `Control` a `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Vrací komponentu. Při pokusu o získání nedefinovaného potomka je zavolána továrna `createComponent($name)`. Metoda `createComponent($name)` zavolá v aktuální komponentě metodu `createComponent` a jako parametr jí předá název komponenty. Vytvořená komponenta je poté přidána do aktuální komponenty jako její potomek. Těmto metodám říkáme továrny na komponenty a mohou je implementovat potomci třídy `Container`. - - -Iterování nad potomky ---------------------- - -K iterování slouží metoda [getComponents($deep = false, $type = null)|api:Nette\ComponentModel\Container::getComponents()]. První parametr určuje, zda se mají komponenty procházet do hloubky (neboli rekurzivně). S hodnotou `true` tedy nejen projde všechny komponenty, jichž je rodičem, ale také potomky svých potomků atd. Druhý parametr slouží jako volitelný filtr podle tříd nebo rozhraní. - -```php -foreach ($form->getComponents(true, Nette\Forms\IControl::class) as $control) { - if (!$control->getRules()->validate()) { - // ... - } -} -``` - - -Monitorování předků -=================== - -Komponentový model Nette umožňuje velmi dynamickou práci se stromem (komponenty můžeme vyjímat, přesouvat, přidávat), proto by byla chyba se spoléhat na to, že po vytvoření komponenty je hned (v konstruktoru) znám rodič, rodič rodiče atd. Většinou totiž rodič při vytvoření vůbec známý není. - -Jak poznat, kdy byla komponenta připojena do stromu presenteru? Sledovat změnu rodiče nestačí, protože k presenteru mohl být připojen třeba rodič rodiče. Pomůže metoda [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Každá komponenta může monitorovat libovolný počet tříd a rozhraní. Připojení nebo odpojení je ohlášeno zavoláním callbacku `$attached` resp. `$detached`, a předáním objektu sledované třídy. - -Pro lepší pochopení příklad: třída `UploadControl`, reprezentující formulářový prvek pro upload souborů v Nette Forms, musí formuláři nastavit atribut `enctype` na hodnotu `multipart/form-data`. V době vytvoření objektu ale k žádnému formuláři připojena být nemusí. Ve kterém okamžiku tedy formulář modifikovat? Řešení je jednoduché - v konstruktoru se požádá o monitoring: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -a jakmile je formulář k dispozici, zavolá se callback. (Dříve se místo něj používala společná metoda `attached` resp. `detached`). - - -{{leftbar: nette:@menu-topics}} diff --git a/component-model/en/@home.texy b/component-model/en/@home.texy deleted file mode 100644 index 76a116ce1c..0000000000 --- a/component-model/en/@home.texy +++ /dev/null @@ -1,74 +0,0 @@ -Component Model -*************** - -.[perex] -An important concept in Nette is the component. We insert [visual interactive components |application:components] into pages, forms or all their elements are also components. There are two basic classes from which all these components inherit, are part of the `nette/component-model` package and are responsible for creating the component tree hierarchy. - - -Component -========= -[api:Nette\ComponentModel\Component] is the common ancestor of all components. It contains the `getName()` method returning the name of the component and the `getParent()` method returning its parent. Both can be set with the `setParent()` method - the first parameter is the parent and the second is the component name. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Searches up the hierarchy for an object of the desired class or interface. For example, `$component->lookup(Nette\Application\UI\Presenter::class)` returns presenter if the component is connected to it, despite several levels. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Returns the so-called path, which is a string formed by concatenating the names of all components on the path between the current component and the component being searched for. So, for example, `$component->lookupPath(Nette\Application\UI\Presenter::class)` returns the unique identifier of the component relative to the presenter. - - -Container -========= -[api:Nette\ComponentModel\Container] is the parent component, i.e. the component containing the children and thus forming the tree structure. It has methods for easily adding, retrieving and removing components. It is the ancestor of, for example, the form or classes `Control` and `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Returns a component. Attempt to call undefined child causes invoking of factory [createComponent($name)|api:Nette\ComponentModel\Container::createComponent()]. Method `createComponent($name)` invokes method `createComponent` in current component and it passes name of the component as a parameter. Created component is then passed to current component as its child. We call theese component factories, they can be implemented in classes inherited from `Container`. - - -Iterating over Children ------------------------ - -The [getComponents($deep = false, $type = null) |api:Nette\ComponentModel\Container::getComponents()] method is used for iteration. The first parameter specifies whether to traverse the components in depth (or recursively). With `true`, it not only iterates all its children, but also all children of its children, etc. Second parameter servers as an optional filter by class or interface. - -```php -foreach ($form->getComponents(true, Nette\Forms\IControl::class) as $control) { - if (!$control->getRules()->validate()) { - // ... - } -} -``` - - -Monitoring of Ancestors -======================= - -The Nette component model allows for very dynamic tree work (we can remove, move, add components), so it would be a mistake to rely on the fact that after creating a component, the parent, parent's parent, etc. are known immediately (in the constructor). Usually the parent is not known at all when the component is created. - -How to find out when a component has been added to the presenter tree? Keeping track of the parent change is not enough, because the parent of the parent could have been attached to the presenter, for example. The [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()] method can help. Each component can monitor any number of classes and interfaces. Connection or disconnection is announced by calling the callbacks `$attached` and `$detached`, respectively, and passing the object of the monitored class. - -An example: Class `UploadControl`, representing form element for uploading files in Nette Forms, has to set form's attribute `enctype` to value `multipart/form-data`. But in the time of the creation of the object it does not have to be attached to any form. When to modify the form? The solution is simple - we create a request for monitoring in the constructor: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -and when the form is available, the callback is called. (Previously, the common methods `attached` and `detached` were used instead.) - - -{{leftbar: nette:@menu-topics}} diff --git a/component-model/meta.json b/component-model/meta.json deleted file mode 100644 index b4b89b8c38..0000000000 --- a/component-model/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "4.0", - "repo": "nette/component-model", - "composer": "nette/component-model" -} diff --git a/contributing/cs/@home.texy b/contributing/cs/@home.texy deleted file mode 100644 index e7c4099f08..0000000000 --- a/contributing/cs/@home.texy +++ /dev/null @@ -1 +0,0 @@ -{{redirect:code}} diff --git a/contributing/cs/@left-menu.texy b/contributing/cs/@left-menu.texy deleted file mode 100644 index 9bd44bb213..0000000000 --- a/contributing/cs/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Zapojte se -********** -- [Přispívání do kódu |code] -- [Psaní dokumentace |documentation] -- [Kódovací standard |coding-standard] diff --git a/contributing/cs/code.texy b/contributing/cs/code.texy deleted file mode 100644 index abf029247e..0000000000 --- a/contributing/cs/code.texy +++ /dev/null @@ -1,108 +0,0 @@ -Přispívání do kódu -****************** - -Nette Framework používá Git a [GitHub |https://github.com/nette/nette] jako úložiště pro kód. Nejlepší způsob, jak přispět, je provést změny ve vaší větvi a poté poslat pull request (PR) na GitHub. Tento dokument shrnuje hlavní kroky jak na to. - - -Příprava prostředí -================== - -Nejprve si [forkněte |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] [Nette z GitHubu |https://github.com/nette]. Pečlivě [nastavte |https://help.github.com/en/github/getting-started-with-github/set-up-git] prostředí Gitu, nakonfigurujte své uživatelské jméno a e-mail, tyto údaje vás budou identifikovat v historii změn kódu. - - -Editace kódu -============ - -Než začnete programovat, vytvořte si novou větev. -```shell -git checkout -b new_branch_name -``` - -A můžete začít upravovat kód. - - -Testování změn -============== - -Nainstalujte si Nette Tester. Nejjednodušší je zavolat `composer install` v kořenovém adresáři repositáře. Nyní byste měli být schopni spustit testy pomocí `./vendor/bin/tester` v Unixovém terminálu nebo `vendor\bin\tester` v terminálu pod Windows. - -Některé testy mohou selhat kvůli chybějícímu php.ini. Proto byste měli volat Tester s parametrem -c a zadat cestu k php.ini, například `./vendor/bin/tester -c ./tests/php.ini`. - -Poté, co funguje spouštění testů, můžete implementovat vlastní změny v kódu. Další informace o testování pomocí nástroje Nette Tester najdete v [jeho dokumentaci |tester:]. - - -Coding Standards -================ - -Váš kód musí splňovat [coding standard] používaný v Nette Framework. Je to snadné, protože existuje automatický nástroj pro kontrolu a opravu kódu. Vyžaduje PHP 7.1 a lze jej nainstalovat přes Composer do vámi zvolené globální složky: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Nyní byste měli mít možnost spustit nástroj v terminálu. Tímto příkazem například zkontrolujete a opravíte kód ve složkách `src` a `tests` v aktuálním adresáři: - -```shell -/path/to/nette-coding-standard/ecs check src tests --config /path/to/nette-coding-standard/coding-standard-php71.yml --fix -``` - - -Komitování změn -=============== - -Po úpravě kódu jej tzv. komitujete. Komitů klidně vytvořte několik, pro každý logický krok jeden. Každý commit by měl být smysluplný sám o sobě. Měl by zahrnovat i testy. - -Zkontrolujte prosím, zda váš kód odpovídá pravidlům: -- Kód nevytváří žádné chyby -- Neporušuje žádné testy. -- Změny v kódu jsou testované. -- Neděláte zbytečné změny v bílém prostoru. - -Popis komitu by měl odpovídat formátu `Latte: fixed multi-template rendering [Closes #69]`, tj. -- oblast následovaná dvojtečkou -- účel commitu v minulém čase, je-li to možné, začněte slovem: "added .(přidaná nová vlastnost)", "fixed .(oprava)", "refactored .(změna v kódu beze změny chování)", changed, removed -- případná vazba na issue tracker -- pokud commit přeruší zpětnou kompatibilitu, doplňte "BC break" -- za subjektem může být jeden volný řádek a podrobnější popis včetně odkazů na fórum. - - -Posílání Pull requestu -====================== - -Pokud jste se změnami spokojeni, odešlete je na GitHub. - -```shell -git push původu new_branch_name -``` - -Kód už je veřejně dostupný, ale musíte je odeslat do hlavní větve (masteru) v repozitáři Nette. Udělejte tzv. [pull request |https://help.github.com/articles/creating-a-pull-request]. -Každá žádost má název a popis. Uveďte prosím výstižný název. Například "Zabezpečení signálů proti útoku CSRF". - -Popis pull requestu by měl obsahovat další specifické informace: -``` -- bug fix? yes/no -- new feature? yes/no -- BC break? yes/no -- doc PR: nette/docs#??? -``` - -Upravte prosím tabulku informací tak, aby odpovídala vašemu pull requestu. -- uveďte, jestli jde o novou funkci, nebo o bugfix -- odkažte na případnou **související issue**, která bude uzavřena po schválení pull requestu. -- uveďte, zda požadavek potřebuje **dokumentační změny**, pokud ano, uveďte odkazy na příslušné issue. Změnu dokumentace nemusíte dělat okamžitě, ale pull request nebude přijat, pokud je zapotřebí změnit dokumentaci. Změna dokumentace musí být připravena pro anglickou dokumentaci, jiné jazykové mutace jsou nepovinné. -- uveďte, zda změna v kódu způsobí **BC break**. Vezměte prosím v úvahu všechno, co jej může způsobit. - -Konečná tabulka by mohla vypadat takto: -``` -- bug fix? no -- new feature? yes issue #123 -- BC break? no -``` - - -Přepracování změn -================= - -Je běžné, že budete dostávat komentáře s připomínkami. Sledujte navrhované změny a zapracujte je. Navrhované změny můžete přidávat jako nové commity a sloučit je s předchozími. Viz kapitola [Interactive rebase |https://help.github.com/en/github/using-git/about-git-rebase] na stránce GitHubu. Poté opět odešlete commity na GitHub a vše se automaticky objeví v pull requestu. - -{{priority: -1}} diff --git a/contributing/cs/coding-standard.texy b/contributing/cs/coding-standard.texy deleted file mode 100644 index 9ff82c6197..0000000000 --- a/contributing/cs/coding-standard.texy +++ /dev/null @@ -1,126 +0,0 @@ -Kódovací standard -***************** - -Tento dokument popisuje pravidla a doporučení pro vývoj Nette. Při přispívání kódu do Nette je musíte dodržovat. Nejjednodušší způsob, jak to udělat, je napodobit existující kód. -Jde o to, aby veškerý kód vypadal, jako by ho napsal jeden člověk . - -Nette Coding Standard odpovídá [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] se dvěma hlavními výjimkami: pro odsazení používá [#tabulátory místo mezer] a pro [konstanty tříd používá PascalCase|https://blog.nette.org/cs/za-mene-kriku-v-kodu]. - - -Obecná pravidla -=============== - -- Každý soubor PHP musí obsahovat `declare(strict_types=1)` -- Dva prázdné řádky se používají k oddělení metod pro lepší čitelnost. -- Důvod použití shut-up operátoru musí být zdokumentován: `@mkdir($dir); // @ - adresář může existovat`. -- Pokud je použit slabě typizovaný operátor porovnání (tj. `==`, `!=`, ...), musí být zdokumentován záměr: `// == přijmout null` -- Do jednoho souboru `exceptions.php` můžete zapsat více výjimek. -- Rozhraní nespecifikují viditelnost metod, protože jsou vždy veřejné. -- Každá vlastnost, metoda a parametr musí mít dokumentovaný typ. Buď nativně, nebo pomocí anotace. -- Pole se musí zapisovat krátkým zápisem. -- K ohraničení řetězce by se měla používat jednoduchá uvozovka, s výjimkou případů, kdy samotný literál obsahuje apostrofy. - - -Pojmenovací konvence -==================== - -- Nepoužívejte zkratky, pokud není celý název příliš dlouhý. -- U dvoupísmenných zkratek používejte velká písmena, u delších zkratek pascal/camel. -- Pro název třídy používejte podstatné jméno nebo slovní spojení. -- Názvy tříd musí obsahovat nejen specifičnost (`Array`), ale také obecnost (`ArrayIterator`). Výjimkou jsou atributy jazyka PHP. -- "Konstanty tříd a enumy by měly používat PascalCaps":https://blog.nette.org/cs/za-mene-kriku-v-kodu. -- "Rozhraní a abstraktní třídy by neměly obsahovat předpony nebo přípony":https://blog.nette.org/cs/predpony-a-pripony-do-nazvu-rozhrani-nepatri jako `Abstract`, `Interface` nebo `I`. - - -Wrapping and Braces -=================== - -Nette Coding Standard odpovídá PSR-12 (resp. PER Coding Style), v některých bodech jej doplňuje nebo upravuje: - -- arrow funkce se píší bez mezery před závorkou, tj. `fn($a) => $b` -- nevyžaduje se prázdný řádek mezi různými typy `use` import statements -- návratový typ funkce/metody a úvodní složená závorka by měly být umístěny na samostatných řádcích pro lepší čitelnost: - -```php - public function find( - string $dir, - array $options, - ): array - { - // tělo metody - } -``` - - -Bloky dokumentace (phpDoc) -========================== - -Hlavní pravidlo: Nikdy neduplikujte žádné informace v signatuře, jako je typ parametru nebo návratový typ, bez přidané hodnoty. - -Dokumentační blok pro definici třídy: - -- Začíná popisem třídy. -- Následuje prázdný řádek. -- Následují anotace `@property` (nebo `@property-read`, `@property-write`), jedna po druhé. Syntaxe je: anotace, mezera, typ, mezera, $jméno. -- Následují anotace `@method`, jedna po druhé. Syntaxe je: anotace, mezera, návratový typ, mezera, jméno(typ $param, ...). -- Anotace `@author` se vynechává. Autorství se uchovává v historii zdrojového kódu. -- Lze použít anotace `@internal` nebo `@deprecated`. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Dokumentační blok pro vlastnost, který obsahuje pouze anotaci `@var`, by měl být jednořádkový: - -```php -/** @var string[] */ -private array $name; -``` - -Dokumentační blok pro definici metody: - -- Začíná krátkým popisem metody. -- Žádný prázdný řádek. -- Anotace `@param` po jednotlivých řádcích. -- Anotace `@return`. -- Anotace `@throws`, jedna po druhé. -- Lze použít anotace `@internal` nebo `@deprecated`. - -Za každou anotací následuje jedna mezera, s výjimkou `@param`, za kterou pro lepší čitelnost následují dvě mezery. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Tabulátory místo mezer -====================== - -Tabulátory mají oproti mezerám několik výhod: - -- velikost odstupu lze v editorech a na "webu":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size přizpůsobit -- nevnucují kódu uživatelovu preferenci velikosti odsazení, takže kód je lépe přenositelný -- lze je napsat jedním stiskem klávesy (kdekoli, nejen v editorech, které mění tabulátory na mezery) -- odsazování je jejich smyslem -- respektují potřeby zrakově postižených a nevidomých kolegů - -Používáním tabulátorů v našich projektech umožňujeme přizpůsobení šířky, které se může většině lidí zdát jako zbytečnost, ale pro lidi se zrakovým postižením je nezbytné. - -Pro nevidomé programátory, kteří používají braillské displeje, představuje každá mezera jednu braillskou buňkou. Pokud je tedy výchozí odsazení 4 mezery, odsazení 3. úrovně plýtvá 12 cennými braillskými buňkami ještě před začátkem kódu. -Na 40buňkovém displeji, který se u notebooků používá nejčastěji, je to více než čtvrtina dostupných buněk, které jsou promrhány bez jakékoliv informace. - - -{{priority: -1}} diff --git a/contributing/cs/documentation.texy b/contributing/cs/documentation.texy deleted file mode 100644 index 6e51001407..0000000000 --- a/contributing/cs/documentation.texy +++ /dev/null @@ -1,55 +0,0 @@ -Psaní dokumentace -***************** - -.[perex] -Přispívání do dokumentace je jednou z mnoha cest, jak lze pomoci Nette. Zároveň jde také o jednu z nejpřínosnějších činností, neboť pomáháte druhým frameworku porozumět. - - -Jak psát? ---------- - -Dokumentace je určena především lidem, kteří se s tématem teprve seznamují. Proto by měla splňovat několik důležitých bodů: - -- **Při psaní začněte od jednoduchého a obecného, k pokročilejším tématům přejděte až na konci.** -- Uvádějte jen ty informace, které uživatel skutečně k danému tématu potřebuje vědět. -- Ověřte si, že vaše informace jsou skutečně pravdivé. Před uvedením kódu příklad nejprve vyzkoušejte. -- Buďte struční - co napíšete, zkraťte na polovinu. A pak klidně ještě jednou. -- Snažte se věc co nejlépe vysvětlit. Zkuste například téma nejprve vysvětlit kolegovi. - -Tyto body mějte na paměti po celou dobu psaní. Další tipy naleznete v článku [Píšeme pro web |https://www.lupa.cz/clanky/piseme-pro-web/]. Dokumentace je psaná v [Texy! |https://texy.info], proto se naučte jeho [syntax]. Pro náhled článku během jeho psaní můžete použít editor dokumentace na adrese [https://editor.nette.org/]. - -Kromě výše uvedených bodů také dodržujte následující zásady: - -- Primárním jazykem je angličtina, vaše změny by tedy měly být v obou jazycích. Pokud angličtina není vaší silnou stránkou, použijte [DeepL Translator |https://www.deepl.com/translator] a ostatní vám váš text zkontrolují. -- V textu dokumentace spíše "mykáme" a jsme zdvořilí. -- V příkladech dodržujte [Coding Standard]. -- Názvy proměnných, tříd a metod pište anglicky. -- Namespaces stačí uvádět při první zmínce. -- Kód se snažte formátovat tak, aby se nezobrazovaly posuvníky. -- Šetřete zvýrazňovači všeho druhu, od tučného písma po rámečky `.[note]`. -- Z dokumentace se odkazujte pouze na dokumentaci nebo `www`. - - -Struktura dokumentace ---------------------- - -Celá dokumentace je umístěna na GitHubu v repositáři [nette/docs |https://github.com/nette/docs]. Tento repositář je rozdělen do větví podle verze dokumentace, například větev `doc-3.1` obsahuje dokumentaci pro verzi 3.1. A dále je tu větev `nette.org`, ve které je obsah ostatních subdomén webu nette.org. - -Každá větev je pak rozdělena do několika složek: - -* `cs` a `en`: obsahuje soubory dokumentace pro jednotlivé jazykové verze -* `files`: obrázky, které je možné do stránek v dokumentaci vkládat - -Cesta k souboru bez přípony odpovídá URL adrese stránky v dokumentaci. Soubor `cs/quickstart/single-post.texy` tedy bude mít adresu `doc.nette.org/cs/quickstart/single-post`. - - -Přispívání do dokumentace -------------------------- - -Pro přispívání do dokumentace je nutné mít účet na [GitHub|https://github.com] a znát základy práce s verzovacím systémem Git. Pokud s gitem nekamarádíte, můžete se podívat na rychlý návod: [git - the simple guide |https://rogerdudler.github.io/git-guide/], nebo si pomoci některým z mnoha grafických nástrojů: [GIT - GUI clients |https://git-scm.com/downloads/guis]. - -Jednoduché změny můžete provádět přímo v rozhraní GitHubu. Vhodnější je ale vytvořit si fork repositáře [nette/docs |https://github.com/nette/docs] a ten si naklonovat na počítač. Poté v příslušné větvi proveďte změny, změnu commitněte, pushněte do svého repositáře na GitHub a pošlete pull request do původního repositáře `nette/docs`. Pamatujte, že hlavním jazykem dokumentace je angličtina, změny proto provádějte v obou jazycích. - -Před každým pull requestem je dobré si spustit [Code-Checker |code-checker:], který nám zkontroluje přebytečné mezery v textu. - -{{priority: -1}} diff --git a/contributing/cs/syntax.texy b/contributing/cs/syntax.texy deleted file mode 100644 index 881397f647..0000000000 --- a/contributing/cs/syntax.texy +++ /dev/null @@ -1,140 +0,0 @@ -Wiki syntax -*********** - -Wiki používá [Texy syntaxi |https://texy.info/cs/syntax] s některými rozšířeními. - - -Odkazy -====== - -Pro interní odkazy je preferovaný zápis v hranatých závorkách `[odkaz]`, lze použít také zápis pomocí uvozovek `"odkaz":www.example.com`. - -- `[Název stránky]` -> [Název stránky] -- `[adresář/Název stránky]` -> [adresář/Název stránky] -- `[/Název stránky]` -> [/Název stránky] (absolutní cesta) -- `[/@home]` nebo `[/]` -> [/] -- `[en/Název stránky]` -> [en/Název stránky] (s uvedením jazyka) -- `[www:Název stránky]` -> [www:Název stránky] (odkaz do jiné knihy se stejným jazykem) -- `[www:en/Název stránky]` -> [www:en/Název stránky] (odkaz do jiné knihy s uvedením jazyka) -- `[www:homepage]` -> [www:homepage] (homepage jiné knihy se stejným jazykem) -- `[www:en]` -> [www:en] (homepage jiné knihy s uvedením jazyka) - -Ve všech uvedených případech lze odlišit text odkazu od odkazované stránky pomocí svislítka: - -- `[text odkazu |Název stránky]` -> [text odkazu |Název stránky] - - -Odkazy na titulky ------------------ - -Lze se také odkazovat na jednotlivé titulky uvnitř stránky pomocí znaku `#`. - -- `[Název stránky#Odkazy na titulky]` -> [Název stránky#Odkazy na titulky] -- `[#Odkazy na titulky]` -> [#Odkazy na titulky] (odkaz na titulek v aktuální stránce) -- `[text odkazu |#Odkazy na titulky]` -> [text odkazu |#Odkazy na titulky] - - -Odkazy do API dokumentace -------------------------- - -Vždy uvádějte pouze pomocí tohoto zápisu: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::REQUIRED]` -> [api:Nette\Forms\Form::REQUIRED] - -Název třídy včetně jmenného prostoru uvádějte prosím co nejméně (například při první zmínce), pak už jmenný prostor vynechávejte. Opět s využitím svislítka: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Odkazy do PHP dokumentace -------------------------- - -- `[php:substr]` -> [php:substr] (do PHP dokumentace) - - -Zdrojový kód -============ - -Bloky kódu začínají ```jazyk a končí ```. Podporované jazyky jsou `php`, `html`, `css`, `js` and `sql` (pro Latte používejte ```html). Odsazujte výhradně tabulátorem. Lze uvést i jméno souboru ```php .{file:ArrayTest.php} - -``` - ```php - public function renderPage($id) - { - ... - } - ``` -``` - - -Nadpisy -======= - -Nejvyšší nadpis (tedy název stránky) podtrhněte hvězdičkami. Pro oddělení sekcí používejte rovnítka. Nadpisy podtrhujte rovnítky a poté pomlčkami: - -``` -MVC Aplikace & presentery -************************* -... - - -Tvorba odkazů -============= -... - - -Odkazy v šablonách ------------------- -... -``` - - -Rámečky a styly -=============== - -Perex označíme třídou `.[perex]` .[perex] - -Poznámku označíme třídou `.[note]` .[note] - -Tip označíme třídou `.[tip]` .[tip] - -Varování označíme třídou `.[caution]` .[caution] - -Důraznější varování označíme třídou `.[warning]` .[warning] - -Číslo verze `.{data-version:2.4.10}` .{data-version:2.4.10} - -Třídy zapisujte před řádkem: - -``` -.[perex] -Tohle je perex. -``` - -Uvědomte si prosím, že rámečky jako `.[tip]` "tahají" oči, tudíž se používají pro zdůraznění, nikoliv pro méně podstatné informace. Proto jejich používám maximálně šetřte. - - -Obsah -===== - -Obsah (odkazy v pravém menu) je automaticky generovaný pro všechny stránky, jejichž velikost přesáhne 4 000 bytů, přičemž toho výchozí chování je možné upravit pomocí [#meta značky] `{{toc}}`. Text tvořící obsah se bere standardně přímo z textu nadpisů, ale pomocí modifikátoru `.{toc}` je možné zobrazit v obsahu jiný text, což se hodí hlavně pro delší nadpisy. - -``` - - -Dlouhý a inteligentní nadpis .{toc: Libovolný jiný text zobrazený v obsahu} -=========================================================================== -``` - - -Meta značky -=========== - -- nastavení vlastního názvu stránky (v `` a drobečkové navigaci) `{{title: Jiný název}}` -- přesměrování `{{redirect: pla:cs}}` - viz [#odkazy] -- vynucení `{{toc}}` či zakázání `{{toc: no}}` automatického obsahu (boxík s odkazy na jednotlivé nadpisy) - -{{priority: -1}} diff --git a/contributing/en/@home.texy b/contributing/en/@home.texy deleted file mode 100644 index e7c4099f08..0000000000 --- a/contributing/en/@home.texy +++ /dev/null @@ -1 +0,0 @@ -{{redirect:code}} diff --git a/contributing/en/@left-menu.texy b/contributing/en/@left-menu.texy deleted file mode 100644 index 88f018a322..0000000000 --- a/contributing/en/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Get Involved -************ -- [Contributing to Code |code] -- [Writing the Documentation |documentation] -- [Coding Standards |coding-standard] diff --git a/contributing/en/code.texy b/contributing/en/code.texy deleted file mode 100644 index 74c4a6cd20..0000000000 --- a/contributing/en/code.texy +++ /dev/null @@ -1,108 +0,0 @@ -Contributing to Code -******************** - -Nette Framework uses Git and [GitHub |https://github.com/nette/nette] for maintaining the code base. The best way to contribute is to commit your changes to your own fork and then make a pull request on GitHub. This document summarize the major steps for successful contributing. - - -Preparing Environment -===================== - -Start with [forking |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] [Nette on GitHub |https://github.com/nette]. Carefully [set up |https://help.github.com/en/github/getting-started-with-github/set-up-git] your local Git environment, configure your username and email, these credentials will identify your changes in Nette Framework history. - - -Working on Your Patch -===================== - -Before you start working on your patch, create a new branch for your changes. -```shell -git checkout -b new_branch_name -``` - -You can work on your code change. - - -Testing Your Changes -==================== - -You need to install Nette Tester. The easiest way is to call `composer install` in repository root. Now you should be able to run tests with `./vendor/bin/tester` in the unix-like terminal or `vendor\bin\tester` in the Windows terminal. - -Some tests may fail due to missing php.ini. Therefore you should call the runner with parameter -c and specify the path to php.ini, for example `./vendor/bin/tester -c ./tests/php.ini`. - -After you are able to run the tests, you can implement your own or change the failing to match the new behavior. Read more about testing with Nette Tester in [documentation page |tester:]. - - -Coding Standards -================ - -Your code must follow [coding standard] used in Nette Framework. It is easy because there is automated checker & fixer. It requires PHP 7.1 and can be installed via Composer to your chosen global directory: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Now you should be able to run tool in the terminal. For example, this command checks and fixes code in folders `src` and `tests` in current directory: - -```shell -/path/to/nette-coding-standard/ecs check src tests --config /path/to/nette-coding-standard/coding-standard-php71.yml --fix -``` - - -Committing the Changes -====================== - -After you have changed the code, you have to commit your changes. Create more commits, one for each logical step. Each commit should have been usable as is - without other commits. So, the appropriate tests should be also included in the same commit. - -Please, double-check your code fits the rules: -- Code does not generate any errors -- Your code does not break any tests. -- Your code change is tested. -- You are not committing useless white-space changes. - -Commit message should follow format `Latte: fixed multi template rendering [Closes # 69]` ie: -- an area followed by a colon -- the purpose of the commit in the past, if possible, start with "added.", "fixed.", "refactored.", changed, removed -- eventual link to issue tracker -- if commit cancels backward compatibility, add "BC break" -- there may be one free line after the subject and a more detailed description including links to the forum. - - -Pull-Requesting the Commits -=========================== - -If you are satisfied with your code changes & commits, you have to push you commits to GitHub. - -```shell -git push origin new_branch_name -``` - -Changes are present publicly, however, you have to propose your changes for integration into master branch of Nette. To do that, [make a pull request |https://help.github.com/articles/creating-a-pull-request]. -Each pull request has a title and a description. Please provide some describing title. It's often similar to the branch name, for example "Securing signals against CSRF attack." - -Pull request description should have contain some more specific information about your code changes: -``` -- bug fix? yes/no <!-- #issue numbers, if any --> -- new feature? yes/no -- BC break? yes/no -- doc PR: nette/docs#??? <!-- highly welcome, see https://nette.org/en/writing --> -``` - -Please change the information table to fit your pull request. Comments to each list item: -- Says if the pull request adds **feature** or it is a **bugfix**. -- References eventually **related issue**, which will be closed after merging the pull request. -- Says if the pull request needs the **documentation changes**, if yes, provide references to the appropriate pull requests. You don't have to provide the documentation change immediately, however, the pull request won't be merged if the documentation change is needed. The documentation change must be prepared for English documentation, other language mutations are optional. -- Says if the pull request creates **a BC break**. Please, consider everything which changes public interface as a BC break. - -The final table could look like: -``` -- bug fix? no -- new feature? yes issue #123 -- BC break? no -``` - - -Reworking Your Changes -====================== - -It is really common to receive comments to your code change. Please, try to follow proposed changes and rework your commits to do that. You can commit proposed changes as new commits and then squash them to the previous ones. See [Interactive rebase |https://help.github.com/en/github/using-git/about-git-rebase] chapter on GitHub. After rebasing your changes, force-push your changes to your remote fork, everything will be automatically propagate to the pull request. - -{{priority: -1}} diff --git a/contributing/en/coding-standard.texy b/contributing/en/coding-standard.texy deleted file mode 100644 index 12bf67756f..0000000000 --- a/contributing/en/coding-standard.texy +++ /dev/null @@ -1,126 +0,0 @@ -Coding Standard -*************** - -This document describes rules and recommendations for developing Nette. When contributing code to Nette, you must follow them. The easiest way how to do it is to imitate the existing code. -The idea is to make all the code look like it was written by one person. .[perex] - -Nette Coding Standard corresponds to [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] with two main exceptions: it uses [#tabs instead of spaces] for indentation, and uses [PascalCase for class constants|https://blog.nette.org/en/for-less-screaming-in-the-code]. - - -General Rules -============= - -- Every PHP file must contain `declare(strict_types=1)` -- Two empty lines are used to separate methods for better readability. -- The reason for using shut-up operator must be documented: `@mkdir($dir); // @ - directory may exist` -- If weak typed comparison operator is used (ie. `==`, `!=`, ...), the intention must be documented: `// == to accept null` -- You can write more exceptions into one file `exceptions.php` -- Interfaces do not specify method visibility because they are always public. -- Every properties, methods and parameters must have documented type. Either natively or via annotation. -- Arrays must be written by short notation. -- The single quote should be used to demarcate the string, except when a literal itself contains apostrophes. - - -Naming Conventions -================== - -- Avoid using abbreviations unless the full name is excessive. -- Use uppercase for two-letter abbreviations, and pascal/camel case for longer abbreviations. -- Use a noun or noun phrase for class name. -- Class names must contain not only specificity (`Array`) but also generality (`ArrayIterator`). The exception are PHP attributes. -- "Class constants and enums should use PascalCaps":https://blog.nette.org/en/for-less-screaming-in-the-code. -- "Interfaces and abstract classes should not contain prefixes or postfixes":https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names like `Abstract`, `Interface` or `I`. - - -Wrapping and Braces -=================== - -Nette Coding Standard corresponds to PSR-12 (or PER Coding Style), in some points it specifies it more or modifies it: - -- arrow functions are written without a space before the parenthesis, i.e. `fn($a) => $b` -- no empty line is required between different types of `use` import statements -- the return type of the function/method and the opening parenthesis should be placed on separate lines for better readability: - -```php - public function find( - string $dir, - array $options, - ): array - { - // method body - } -``` - - -Documentation Blocks (phpDoc) -============================= - -The main rule: never duplicate any signature information like parameter type or return type with no added value. - -Documentation block for class definition: - -- Starts by a class description. -- Empty line follows. -- The `@property` (or `@property-read`, `@property-write`) annotations follow, one by line. Syntax is: annotation, space, type, space, $name. -- The `@method` annotations follow, one by line. Syntax is: annotation, space, return type, space, name(type $param, ...). -- The `@author` annotation is omitted. The authorship is kept in a source code history. -- The `@internal` or `@deprecated` annotations can be used. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Documentation block for property that contains only `@var` annotation should be in single line: - -```php -/** @var string[] */ -private array $name; -``` - -Documentation block for method definition: - -- Starts by a short method description. -- No empty line. -- The `@param` annotations, one by line. -- The `@return` annotation. -- The `@throws` annotations, one by line. -- The `@internal` or `@deprecated` annotations can be used. - -Every annotation is followed by one space, except for the `@param` which is followed by two spaces for better readability. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Tabs Instead of Spaces -====================== - -Tabs have several advantages over spaces: - -- indent size is customisable in editors & "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- they do not impose the user's indentation size preference on the code, so the code is more portable -- you can type them with one keystroke (anywhere, not just in editors that turn tabs into spaces) -- indentation is their purpose -- respect the needs of visually impaired and blind colleagues - -By using tabs in our projects, we allow for width customization, which may seem unnecessary to most people, but is essential for people with visual impairments. - -For blind programmers who use braille displays, each space is represented by a braille cell and takes up valuable space. So if the default indentation is 4 spaces, a 3rd level indentation wastes 12 braille cells before the code begins. -On a 40-cell display, which is the most commonly used on laptops, that's more than a quarter of the available cells wasted without any information. - - -{{priority: -1}} diff --git a/contributing/en/documentation.texy b/contributing/en/documentation.texy deleted file mode 100644 index 5b0b12ce2f..0000000000 --- a/contributing/en/documentation.texy +++ /dev/null @@ -1,53 +0,0 @@ -Writing the Documentation -************************* - -.[perex] -Contributing to the documentation is one of the many ways you can help Nette. It is also one of the most rewarding activities, as you help others understand the framework. - - -How to Write? -------------- - -The documentation is primarily intended for people who are just getting familiar with the topic. Therefore, it should meet several important points: - -- **When writing, start with the simple and general, and move to more advanced topics at the end.** -- Provide only the information that the user really needs to know about the topic. -- Verify that your information is actually true. Test the example first before giving the example. -- Be concise - cut what you write in half. And then feel free to do it again. -- Try to explain the matter as well as possible. For example, try explaining the topic to a colleague first. - -Keep these points in mind throughout the writing process. The documentation is written in [Texy! |https://texy.info], so learn its [syntax]. You can use the documentation editor at [https://editor.nette.org/] to preview the article as you write it. - -Among the general writing rules listed earlier, please stick to the following: - -- Your code should be in compliance with the [Coding Standard]. -- Write the names of variables, classes and methods in English. -- Namespaces need only be mentioned at first mention. -- Try to format the code so that scroll bars are not displayed. -- Spare all kinds of highlighters, from boldface to `.[note]` boxes. -- From the documentation, refer only to the documentation or `www`. - - -Documentation Structure ------------------------ - -The full documentation is hosted on GitHub in the [nette/docs |https://github.com/nette/docs] repository. This repository is divided into branches based on the version of the documentation, for example the `doc-3.1` branch contains the documentation for version 3.1. And then there is the `nette.org` branch, which contains the content of the other subdomains of nette.org. - -Each branch is then divided into several folders: - -* `cs` and `en`: contains documentation files for each language version -* `files`: images that can be embedded in the documentation pages - -The path to a file without an extension corresponds to the URL of a page in the documentation. Thus, the file `en/quickstart/single-post.texy` will have the URL `doc.nette.org/en/quickstart/single-post`. - - -Contributing ------------- - -To contribute to the documentation, you must have an account at [GitHub|https://github.com] and know the basics of Git. If you're not familiar with Git, you can check out the quick guide: [git - the simple guide |https://rogerdudler.github.io/git-guide/], or use one of the many graphical tools: [GIT - GUI clients |https://git-scm.com/downloads/guis]. - -You can make simple changes directly in the GitHub interface. However, it is more convenient to create a fork of the repository [nette/docs |https://github.com/nette/docs] and clone it to your computer. Then make changes in the appropriate branch, commit the change, push to your GitHub, and send a pull request to the original `nette/docs` repository. - -Before each pull request, it's a good idea to run [Code-Checker |code-checker:] to check extra whitespace in the text. - -{{priority: -1}} diff --git a/contributing/en/syntax.texy b/contributing/en/syntax.texy deleted file mode 100644 index 03f321bc1c..0000000000 --- a/contributing/en/syntax.texy +++ /dev/null @@ -1,141 +0,0 @@ -Wiki Syntax -*********** - -Wiki uses [Texy syntax |https://texy.info/en/syntax] with several enhancements. - - -Links -===== - -For internal links it is preferred to use square brackets notation `[link]`. It is also possible to use notation with quotes `"link":www.example.com`. - -- `[Page name]` -> [Page name] -- `[dir/Page name]` -> [dir/Page name] -- `[/Page name]` -> [/Page name] (absolute path) -- `[/@home]` or `[/]` -> [/] -- `[en/Page name]` -> [en/Page name] (explicit language specification) -- `[www:Page name]` -> [www:Page name] (different domain, same language) -- `[www:en/Page name]` -> [www:en/Page name] (different domain, specified language) -- `[www:homepage]` -> [www:homepage] (homepage of different domain, same language) -- `[www:en]` -> [www:en] (homepage of different domain, specified language) - - -In all cases it is possibly to specify link text using vertical bar (`|`): - -- `[link text |Page name]` -> [link text |Page name] - - -Links to Headings ------------------ - -It's also possible to target specific heading on page with `#`. - -- `[Page name#Heading]` -> [Page name#Heading] -- `[#Heading]` -> [#Heading] (link to heading on the current page) -- `[link text |#Heading]` -> [link text |#Heading] - - -Links to API Documentation --------------------------- - -Always use the following notations: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::REQUIRED]` -> [api:Nette\Forms\Form::REQUIRED] - -Try to avoid using fully qualified names. Use them only in the first mention. Again with the usage of vertical bar: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Links to PHP Documentation --------------------------- - -- `[php:substr]` -> [php:substr] - - -Source Code -=========== - -Code block starts with <code>```lang</code> and ends with <code>```</code>. Supported languages are `php`, `html`, `css`, `js` and `sql` (use <code>```html</code> for *Latte*). Always use 1 tab for indenting, never spaces. The file name can also be specified <code>```php .{file:ArrayTest.php}</code> - -``` - ```php - public function renderPage($id) - { - ... - } - ``` -``` - - -Headings -======== - -Top heading (page name) underline with stars (`*`). For normal headings use equal signs (`=`) and then hyphens (`-`). - -``` -MVC Applications & Presenters -***************************** -... - - -Link Creation -============= -... - - -Links in Templates ------------------- -... -``` - - -Boxes and Styles -================ - -Lead paragraph marked with class `.[perex]` .[perex] - -Notes marked with class `.[note]` .[note] - -Tip marked with class `.[tip]` .[tip] - -Warning marked with class `.[caution]` .[caution] - -Strong warning marked with class `.[warning]` .[warning] - -Version number `.{data-version:2.4.10}` .{data-version:2.4.10} - -Classes should be written before the related line: - -``` -.[note] -This is a note. -``` - -Please note that boxes such as `.[tip]` draws attention and therefore should be used for emphasizing, not for less important information. - - -Table of Contents -================= - -Table of contents (links in the sidebar) is automatically generated when page is longer than 4 000 bytes. This default behavior can be changed with a `{{toc}}` [meta tag |#meta-tags]. The text for TOC is taken by default from the heading but it is possible to use a different text with a `.{toc}` modifier. This is especially useful for longer headings. - -``` - - -Long and Intelligent Heading .{toc: A Different Text for TOC} -============================================================= -``` - - -Meta Tags -========= - -- setting your own page title (in `<title>` and breadcrumbs) `{{title: Another name}}` -- redirecting `{{redirect: pla:cs}}` - see [#links] -- enforcing `{{toc}}` or disabling `{{toc: no}}` table of content - -{{priority: -1}} diff --git a/contributing/meta.json b/contributing/meta.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/contributing/meta.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/database/cs/@left-menu.texy b/database/cs/@left-menu.texy index 039cedfdf8..6cb0892efa 100644 --- a/database/cs/@left-menu.texy +++ b/database/cs/@left-menu.texy @@ -1,5 +1,12 @@ -Databáze -******** -- [Core] +Nette Database +************** +- [Úvod |guide] +- [SQL přístup |sql way] - [Explorer] +- [Transakce |transactions] +- [Výjimky |exceptions] +- [Reflexe |reflection] +- [Mapování |mapping] - [Konfigurace |configuration] +- [Bezpečnostní rizika |security] +- [Upgrade |upgrading] diff --git a/database/cs/@meta.texy b/database/cs/@meta.texy new file mode 100644 index 0000000000..462d9add80 --- /dev/null +++ b/database/cs/@meta.texy @@ -0,0 +1 @@ +{{sitename: Nette Dokumentace}} diff --git a/database/cs/configuration.texy b/database/cs/configuration.texy index 0bae753b56..021ee3cd11 100644 --- a/database/cs/configuration.texy +++ b/database/cs/configuration.texy @@ -20,7 +20,7 @@ database: password: ... ``` -Vytvoří službu `Nette\Database\Connection` a `Nette\Database\Explorer` pro vrstvu [Database Explorer|explorer]. Databáze se obvykle předává [autowiringem |dependency-injection:autowiring], není-li to možné, použijte názvy služeb `@database.default.connection` resp. `@database.default.explorer`. +Vytvoří služby `Nette\Database\Connection` a `Nette\Database\Explorer`, které si obvykle předáváme [autowiringem |dependency-injection:autowiring], případně odkazem na [jejich název |#Služby DI]. Další nastavení: @@ -49,7 +49,13 @@ database: sqlmode: # (string) # pouze MySQL: nastaví SET NAMES - charset: # (string) výchozí je 'utf8mb4' ('utf8' před verzí 5.5.3) + charset: # (string) výchozí je 'utf8mb4' + + # pouze MySQL: převádí TINYINT(1) na bool + convertBoolean: # (bool) výchozí je false + + # vrací sloupce s datem jako immutable objekty (od verze 3.2.1) + newDateTime: # (bool) výchozí je false # pouze Oracle a SQLite: formát pro ukládání data formatDateTime: # (string) výchozí je 'U' @@ -80,9 +86,23 @@ database: dsn: 'sqlite::memory:' ``` -Každé takto definované spojení vytváří služby zahrnující v názvu i název sekce, tj. `@database.main.connection` & `@database.main.explorer` a dále `@database.another.connection` & `@database.another.explorer`. +Autowiring je zapnutý jen u služeb z první sekce. Lze to změnit pomocí `autowired: false` nebo `autowired: true`. + + +Služby DI +--------- + +Tyto služby se přidávají do DI kontejneru, kde `###` představuje název spojení: + +| Název | Typ | Popis +|---------------------------------------------------------- +| `database.###.connection` | [api:Nette\Database\Connection] | spojení s databází +| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] + + +Pokud definujeme jen jedno spojení, názvy služeb budou `database.default.connection` a `database.default.explorer`. Pokud definujeme více spojení jako v příkladu výše, názvy budou odpovídat sekcím, tj. `database.main.connection`, `database.main.explorer` a dále `database.another.connection` a `database.another.explorer`. -Autowiring je zapnutý jen u služeb z první sekce. Lze to změnit pomocí `autowired: false` nebo `autowired: true`. Neautowirované služby předáváme explicitně: +Neautowirované služby předáváme explicitně odkazem na jejich název: ```neon services: diff --git a/database/cs/core.texy b/database/cs/core.texy deleted file mode 100644 index 7437323a72..0000000000 --- a/database/cs/core.texy +++ /dev/null @@ -1,352 +0,0 @@ -Database Core -************* - -.[perex] -Nette Database Core je základní vrstva pro přístup k databázi, tzv. database abstraction layer. - - -Instalace -========= - -Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Připojení a konfigurace -======================= - -Pro připojení k databázi stačí vytvořit instanci třídy [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Parametr `$dsn` (data source name) je stejný, [jaký používá PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], např. `host=127.0.0.1;dbname=test`. V případě selhání vyhodí výjimku `Nette\Database\ConnectionException`. - -Nicméně šikovnější způsob nabízí [aplikační konfigurace |configuration], kam stačí přidat sekci `database` a vytvoří se potřebné objekty a také databázový panel v [Tracy |tracy:] baru. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Poté objekt spojení [získáme jako službu z DI kontejneru |dependency-injection:passing-dependencies], např.: - -```php -class Model -{ - private $database; - - // pro práci s vrstvou Database Explorer si předáme Nette\Database\Explorer - public function __construct(Nette\Database\Connection $database) - { - $this->database = $database; - } -} -``` - -Více informací o [konfiguraci databáze|configuration]. - - -Dotazy -====== - -Databázové dotazy pokládáme metodou `query()`, která vrací [ResultSet |api:Nette\Database\ResultSet]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} - -echo $result->getRowCount(); // vrací počet řádků výsledku, pokud je znám -``` - -.[note] -Nad `ResultSet` je možné iterovat pouze jednou, pokud potřebujeme iterovat vícekrát, je nutno výsledek převést na pole metodou `fetchAll()`. - -Do dotazu lze velmi snadno přidávat i parametry, všimněte si otazníku: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); - -$database->query('SELECT * FROM users WHERE name = ? AND active = ?', $name, $active); - -$database->query('SELECT * FROM users WHERE id IN (?)', $ids); // $ids je pole -``` - -<div class=warning> -POZOR, nikdy dotazy neskládejte jako řetězce, vznikla by zranitelnost [SQL injection |https://cs.wikipedia.org/wiki/SQL_injection] -/-- -$db->query('SELECT * FROM users WHERE name = ' . $name); // ŠPATNĚ!!! -\-- -</div> - -V případě selhání `query()` vyhodí buď `Nette\Database\DriverException` nebo některého z potomků: - -- [ConstraintViolationException |api:Nette\Database\ConstraintViolationException] - porušení nějakého omezení pro tabulku -- [ForeignKeyConstraintViolationException |api:Nette\Database\ForeignKeyConstraintViolationException] - neplatný cizí klíč -- [NotNullConstraintViolationException |api:Nette\Database\NotNullConstraintViolationException] - porušení podmínky NOT NULL -- [UniqueConstraintViolationException |api:Nette\Database\UniqueConstraintViolationException] - koliduje unikátní index - -Kromě `query()` jsou tu další užitečné funkce: - -```php -// vrátí asociativní pole id => name -$pairs = $database->fetchPairs('SELECT id, name FROM users'); - -// vrátí všechny záznamy jako pole -$rows = $database->fetchAll('SELECT * FROM users'); - -// vrátí jeden záznam -$row = $database->fetch('SELECT * FROM users WHERE id = ?', $id); - -// vrátí přímo hodnotu buňky -$name = $database->fetchField('SELECT name FROM users WHERE id = ?', $id); -``` - -V případě selhání všechny tyto metody vyhodí `Nette\Database\DriverException`. - - -Insert, Update & Delete -======================= - -Parameterem, který vkládáme do SQL dotazu, může být i pole (v takovém případě je navíc možné zástupný symbol `?` vynechat), což se hodí třeba pro sestavení příkazu `INSERT`: - -```php -$database->query('INSERT INTO users ?', [ // tady můžeme otazník vynechat - 'name' => $name, - 'year' => $year, -]); -// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978) - -$id = $database->getInsertId(); // vrátí auto-increment vloženého záznamu - -$id = $database->getInsertId($sequence); // nebo hodnotu sekvence -``` - -Vícenásobný INSERT: - -```php -$database->query('INSERT INTO users', [ - [ - 'name' => 'Jim', - 'year' => 1978, - ], [ - 'name' => 'Jack', - 'year' => 1987, - ] -]); -// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978), ('Jack', 1987) -``` - -Jako parametry můžeme předávat i soubory, objekty DateTime nebo [výčtové typy |https://www.php.net/enumerations]: - -```php -$database->query('INSERT INTO users', [ - 'name' => $name, - 'created' => new DateTime, // nebo $database::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // vloží soubor - 'status' => State::New, // enum State -]); -``` - -Úprava záznamů: - -```php -$result = $database->query('UPDATE users SET', [ - 'name' => $name, - 'year' => $year, -], 'WHERE id = ?', $id); -// UPDATE users SET `name` = 'Jim', `year` = 1978 WHERE id = 123 - -echo $result->getRowCount(); // vrací počet ovlivněných řádků -``` - -Pro UPDATE můžeme využít operátorů `+=` a `-=`: - -```php -$database->query('UPDATE users SET', [ - 'age+=' => 1, // všimněte si += -], 'WHERE id = ?', $id); -// UPDATE users SET `age` = `age` + 1 -``` - -Mazání: - -```php -$result = $database->query('DELETE FROM users WHERE id = ?', $id); -echo $result->getRowCount(); // vrací počet ovlivněných řádků -``` - - -Pokročilé dotazy -================ - -Vložení, nebo úprava záznamu, pokud již existuje: - -```php -$database->query('INSERT INTO users', [ - 'id' => $id, - 'name' => $name, - 'year' => $year, -], 'ON DUPLICATE KEY UPDATE', [ - 'name' => $name, - 'year' => $year, -]); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Všimněte si, že Nette Database pozná, v jakém kontextu SQL příkazu parametr s polem vkládáme a podle toho z něj sestaví SQL kód. Takže z prvního pole sestavil `(id, name, year) VALUES (123, 'Jim', 1978)`, zatímco druhé převedl do podoby `name = 'Jim', year = 1978`. - -Také řazení můžeme ovlivnit polem, v klíčích uvedeme sloupce a hodnotou bude boolean určující, zda řadit vzestupně: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // vzestupně - 'name' => false, // sestupně -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - -Pokud by u neobvyklé konstrukce detekce nezafungovala, můžete formu sestavení určit zástupným symbolem `?` doplněným o hint. Podporovány jsou tyto hinty: - -| ?values | (key1, key2, ...) VALUES (value1, value2, ...) -| ?set | key1 = value1, key2 = value2, ... -| ?and | key1 = value1 AND key2 = value2 ... -| ?or | key1 = value1 OR key2 = value2 ... -| ?order | key1 ASC, key2 DESC - -V klauzuli WHERE se používá operátor `?and`, takže podmínky se spojují operátorem `AND`: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year' => $year, -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND `year` = 1978 -``` - -Což můžeme snadno změnit na `OR` tím, že uvedeme zástupný symbol `?or`: - -```php -$result = $database->query('SELECT * FROM users WHERE ?or', [ - 'name' => $name, - 'year' => $year, -]); -// SELECT * FROM users WHERE `name` = 'Jim' OR `year` = 1978 -``` - -V podmínkách můžeme používat operátory: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name <>' => $name, - 'year >' => $year, -]); -// SELECT * FROM users WHERE `name` <> 'Jim' AND `year` > 1978 -``` - -A také výčty: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => ['Jim', 'Jack'], - 'role NOT IN' => ['admin', 'owner'], // výčet + operátor NOT IN -]); -// SELECT * FROM users WHERE -// `name` IN ('Jim', 'Jack') AND `role` NOT IN ('admin', 'owner') -``` - -Do podmínky také můžeme vložit kus vlastního SQL kódu pomocí tzv. SQL literálu: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Nebo alternativě: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL literál také může mít své parametry: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Díky čemuž můžeme vytvářet zajímavé kombinace: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Proměnný název -============== - -Ještě existuje zástupný symbol `?name`, který využijete v případě, že název tabulky nebo sloupce je proměnnou. (Pozor, nedovolte uživateli manipulovat s obsahem takové proměnné): - -```php -$table = 'blog.users'; -$column = 'name'; -$database->query('SELECT * FROM ?name WHERE ?name = ?', $table, $column, $name); -// SELECT * FROM `blog`.`users` WHERE `name` = 'Jim' -``` - - -Transakce -========= - -Pro práci s transakcemi slouží trojice metod: - -```php -$database->beginTransaction(); // zahájení transakce - -$database->commit(); // potvrzení - -$database->rollback(); // vrácení zpět -``` - -Elegantní způsob nabízí metoda `transaction()`, které předáme callback, který se vykoná v transakci. Pokud během vykonávání dojde k vyhození výjimky, transakce se zahodí, pokud vše proběhne v pořádku, transakce se potvrdí. - -```php -$id = $database->transaction(function ($database) { - $database->query('DELETE FROM ...'); - $database->query('INSERT INTO ...'); - // ... - return $database->getInsertId(); -}); -``` - -Jak vidíte, metoda `transaction()` vrací návratovou hodnotu callbacku. - -Volání `transaction()` může být i zanořeno, což zjednodušuje implementaci nezávislých repozitářů. diff --git a/database/cs/exceptions.texy b/database/cs/exceptions.texy new file mode 100644 index 0000000000..e78dd0108d --- /dev/null +++ b/database/cs/exceptions.texy @@ -0,0 +1,34 @@ +Výjimky +******* + +Nette Database používá hierarchii výjimek. Základní třídou je `Nette\Database\DriverException`, která dědí z `PDOException` a poskytuje rozšířené možnosti pro práci s chybami databáze: + +- Metoda `getDriverCode()` vrací kód chyby od databázového driveru +- Metoda `getSqlState()` vrací SQLSTATE kód +- Metody `getQueryString()` a `getParameters()` umožňují získat původní dotaz a jeho parametry + +Z `DriverException` dědí následující specializované výjimky: + +- `ConnectionException` - signalizuje selhání připojení k databázovému serveru +- `ConstraintViolationException` - základní třída pro porušení databázových omezení, ze které dědí: + - `ForeignKeyConstraintViolationException` - porušení cizího klíče + - `NotNullConstraintViolationException` - porušení NOT NULL omezení + - `UniqueConstraintViolationException` - porušení unikátnosti hodnoty + + +Příklad zachytávání výjimky `UniqueConstraintViolationException`, která nastane, když se snažíme vložit uživatele s emailem, který už v databázi existuje (za předpokladu, že sloupec email má unikátní index). + +```php +try { + $database->query('INSERT INTO users', [ + 'email' => 'john@example.com', + 'name' => 'John Doe', + 'password' => $hashedPassword, + ]); +} catch (Nette\Database\UniqueConstraintViolationException $e) { + echo 'Uživatel s tímto emailem již existuje.'; + +} catch (Nette\Database\DriverException $e) { + echo 'Došlo k chybě při registraci: ' . $e->getMessage(); +} +``` diff --git a/database/cs/explorer.texy b/database/cs/explorer.texy index f3909d4bbe..24044ef827 100644 --- a/database/cs/explorer.texy +++ b/database/cs/explorer.texy @@ -3,529 +3,798 @@ Database Explorer <div class=perex> -Nette Database Explorer (dříve Nette Database Table, NDBT) zásadním způsobem zjednodušuje získávání dat z databáze bez nutnosti psát SQL dotazy. +Explorer nabízí intuitivní a efektivní způsob práce s databází. Stará se automaticky o vazby mezi tabulkami a optimalizaci dotazů, takže se můžete soustředit na svou aplikaci. Funguje ihned bez nastavování. Pokud potřebujete plnou kontrolu nad SQL dotazy, můžete využít [SQL přístup |SQL way]. -- pokládá efektivní dotazy -- nepřenáší zbytečná data -- má elegantní syntax +- Práce s daty je přirozená a snadno pochopitelná +- Generuje optimalizované SQL dotazy, které načítají pouze potřebná data +- Umožňuje snadný přístup k souvisejícím datům bez nutnosti psát JOIN dotazy +- Funguje okamžitě bez jakékoliv konfigurace či generování entit </div> -Používání Database Explorer začíná od tabulky a to zavoláním metody `table()` nad objektem [api:Nette\Database\Explorer]. Jak ho nejsnadněji získat je [popsáno tady |core#Připojení a konfigurace], pokud však používáme Nette Database Explorer samostatně, lze jej [vytvořit i ručně|#Ruční vytvoření Explorer]. + +S Explorerem začnete voláním metody `table()` objektu [api:Nette\Database\Explorer] (detaily k připojení najdete v kapitole [Připojení a konfigurace |guide#Připojení a konfigurace]): ```php -$books = $explorer->table('book'); // jméno tabulky je 'book' +$books = $explorer->table('book'); // 'book' je jméno tabulky ``` -Vrací nám objekt [Selection |api:Nette\Database\Table\Selection], nad kterým můžeme iterovat a projít tak všechny knihy. Řádky jsou instance [ActiveRow |api:Nette\Database\Table\ActiveRow] a data z nich můžeme přímo číst. +Metoda vrací objekt [Selection |api:Nette\Database\Table\Selection], který představuje SQL dotaz. Na tento objekt můžeme navazovat další metody pro filtrování a řazení výsledků. Dotaz se sestaví a spustí až ve chvíli, kdy začneme požadovat data. Například procházením cyklem `foreach`. Každý řádek je reprezentován objektem [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // výpis sloupce 'title' + echo $book->author_id; // výpis sloupce 'author_id' } ``` -Výběr jednoho konkrétního řádku se provádí pomocí metody `get()`, která vrací přímo instanci ActiveRow. +Explorer zásadním způsobem usnadňuje práci s [vazbami mezi tabulkami |#Vazby mezi tabulkami]. Následující příklad ukazuje, jak snadno můžeme vypsat data z provázaných tabulek (knihy a jejich autoři). Všimněte si, že nemusíme psát žádné JOIN dotazy, Nette je vytvoří za nás: ```php -$book = $explorer->table('book')->get(2); // vrátí knihu s id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Kniha: ' . $book->title; + echo 'Autor: ' . $book->author->name; // vytvoří JOIN na tabulku 'author' +} ``` -Pojďme si vyzkoušet jednoduchý příklad. Potřebujeme z databáze vybrat knihy a jejich autory. To je jednoduchý příklad vazby 1:N. Časté řešení je vybrat data jedním SQL dotazem se spojením tabulek pomocí JOINu. Druhou možností je vybrat data odděleně, jedním dotazem knihy, a poté pro každou knihu vybrat jejího autora (např. pomocí foreach cyklu). To může být optimalizováno do dvou požadavků do databáze, jeden pro knihy a druhý pro autory - a přesně takto to dělá Nette Database Explorer. +Nette Database Explorer optimalizuje dotazy, aby byly co nejefektivnější. Výše uvedený příklad provede pouze dva SELECT dotazy, bez ohledu na to, jestli zpracováváme 10 nebo 10 000 knih. -V níže uvedených příkladech budeme pracovat s databázovým schématem na obrázku. Jsou v něm vazby OneHasMany (1:N) (autor knihy `author_id` a případný překladatel `translator_id`, který může mít hodnotu `null`) a vazba ManyHasMany (M:N) mezi knihou a jejími tagy. +Navíc Explorer sleduje, které sloupce se v kódu používají, a načítá z databáze pouze ty, čímž šetří další výkon. Toto chování je plně automatické a adaptivní. Pokud později upravíte kód a začnete používat další sloupce, Explorer automaticky upraví dotazy. Nemusíte nic nastavovat, ani přemýšlet nad tím, které sloupce budete potřebovat - nechte to na Nette. -[Příklad včetně schématu najdete na GitHubu |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Struktura databáze pro uvedené příklady .<> +Filtrování a řazení +=================== -Následující kód vypíše jméno autora každé knihy a všechny její tagy. Jak přesně to funguje si [povíme za chvíli|#Vazby mezi tabulkami]. +Třída `Selection` poskytuje metody pro filtrování a řazení výběru dat. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Přidá podmínku WHERE. Více podmínek je spojeno operátorem AND +| `whereOr(array $conditions)` | Přidá skupinu podmínek WHERE spojených operátorem OR +| `wherePrimary($value)` | Přidá podmínku WHERE podle primárního klíče +| `order($columns, ...$params)` | Nastaví řazení ORDER BY +| `select($columns, ...$params)` | Specifikuje sloupce, které se mají načíst +| `limit($limit, $offset = null)` | Omezí počet řádků (LIMIT) a volitelně nastaví OFFSET +| `page($page, $itemsPerPage, &$total = null)` | Nastaví stránkování +| `group($columns, ...$params)` | Seskupí řádky (GROUP BY) +| `having($condition, ...$params)` | Přidá podmínku HAVING pro filtrování seskupených řádků -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author je řádek z tabulky 'author' +Metody lze řetězit (tzv. [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. - echo 'tags: '; - foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name . ', '; // $bookTag->tag je řádek z tabulky 'tag' - } -} -``` +V těchto metodách můžete také používat speciální notaci pro přístup k [datům ze souvisejících tabulek |#Dotazování přes související tabulky]. -Příjemně vás překvapí, jak efektivně databázová vrstva pracuje. Výše uvedený příklad provede konstantní počet požadavků, které vypadají takto: -```sql -SELECT * FROM `book` -SELECT * FROM `author` WHERE (`author`.`id` IN (11, 12)) -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 4, 2, 3)) -SELECT * FROM `tag` WHERE (`tag`.`id` IN (21, 22, 23)) -``` +Escapování a identifikátory +--------------------------- -Pokud použijete [cache |caching:] (ve výchozím nastavení je zapnutá), nebudou z databáze načítány žádné nepotřebné sloupce. Po prvním dotazu se do cache uloží jména použitých sloupců a dále budou z databáze vybírány pouze ty sloupce, které skutečně použijete: +Metody automaticky escapují parametry a uvozují identifikátory (názvy tabulek a sloupců), čímž zabraňuje SQL injection. Pro správné fungování je nutné dodržovat několik pravidel: -```sql -SELECT `id`, `title`, `author_id` FROM `book` -SELECT `id`, `name` FROM `author` WHERE (`author`.`id` IN (11, 12)) -SELECT `book_id`, `tag_id` FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 4, 2, 3)) -SELECT `id`, `name` FROM `tag` WHERE (`tag`.`id` IN (21, 22, 23)) +- Klíčová slova, názvy funkcí, procedur apod. pište **velkými písmeny**. +- Názvy sloupců a tabulek pište **malými písmeny**. +- Řetězce vždy dosazujte přes **parametry**. + +```php +where('name = ' . $name); // KRITICKÁ ZRANITELNOST: SQL injection +where('name LIKE "%search%"'); // ŠPATNĚ: komplikuje automatické uvozování +where('name LIKE ?', '%search%'); // SPRÁVNĚ: hodnota dosazená přes parametr + +where('name like ?', $name); // ŠPATNĚ: vygeneruje: `name` `like` ? +where('name LIKE ?', $name); // SPRÁVNĚ: vygeneruje: `name` LIKE ? +where('LOWER(name) = ?', $value);// SPRÁVNĚ: LOWER(`name`) = ? ``` -Výběry -====== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Podívejme se na možnosti filtrování a omezování výběru pomocí třídy [api:Nette\Database\Table\Selection]: +Filtruje výsledky pomocí podmínek WHERE. Její silnou stránkou je inteligentní práce s různými typy hodnot a automatická volba SQL operátorů. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Nastaví WHERE s použitím AND jako spojovatele při více než jedné podmínce -| `$table->whereOr($where)` | Nastaví WHERE s použitím OR jako spojovatele při více než jedné podmínce -| `$table->order($columns)` | Nastaví ORDER BY, může být výraz `('column DESC, id DESC')` -| `$table->select($columns)` | Nastaví vrácené sloupce, může být výraz `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Nastaví LIMIT a OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Nastaví stránkování -| `$table->group($columns)` | Nastaví GROUP BY -| `$table->having($having)` | Nastaví HAVING +Základní použití: -Můžeme použít tzv. fluent interface, například `$table->where(...)->order(...)->limit(...)`. Vícenásobné `where` nebo `whereOr` podmínky je spojeny operátorem `AND`. +```php +$table->where('id', $value); // WHERE `id` = 123 +$table->where('id > ?', $value); // WHERE `id` > 123 +$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' +``` +Díky automatické detekci vhodných operátorů nemusíme řešit různé speciální případy. Nette je vyřeší za nás: -where() -------- +```php +$table->where('id', 1); // WHERE `id` = 1 +$table->where('id', null); // WHERE `id` IS NULL +$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) +// lze použít i zástupný otazník bez operátoru: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer automaticky přidá vhodné operátory podle toho, jaká data dostane: +Metoda správně zpracovává i záporné podmínky a prázdné pole: -.[language-php] -| `$table->where('field', $value)` | field = $value -| `$table->where('field', null)` | field IS NULL -| `$table->where('field > ?', $val)` | field > $val -| `$table->where('field', [1, 2])` | field IN (1, 2) -| `$table->where('id = ? OR name = ?', 1, $name)` | id = 1 OR name = 'Jon Snow' -| `$table->where('field', $explorer->table($tableName))` | field IN (SELECT $primary FROM $tableName) -| `$table->where('field', $explorer->table($tableName)->select('col'))` | field IN (SELECT col FROM $tableName) +```php +$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- nic nenalezne +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- nalezene vše +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- nalezene vše +// $table->where('NOT id ?', $ids); Pozor - tato syntaxe není podporovaná +``` -Zástupný symbol (otazník) funguje i bez sloupcového operátoru. Následující volání jsou stejná: +Jako parametr můžeme předat také výsledek z jiné tabulky - vytvoří se poddotaz: ```php -$table->where('id = ? OR id = ?', 1, 2); -$table->where('id ? OR id ?', 1, 2); +// WHERE `id` IN (SELECT `id` FROM `tableName`) +$table->where('id', $explorer->table($tableName)); + +// WHERE `id` IN (SELECT `col` FROM `tableName`) +$table->where('id', $explorer->table($tableName)->select('col')); ``` -Díky tomu lze generovat správný operátor na základě hodnoty: +Podmínky můžeme předat také jako pole, jehož položky se spojí pomocí AND: ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -Selection správně zpracovává i záporné podmínky a umí pracovat také s prázdnými poli: +V poli můžeme použít dvojice klíč => hodnota a Nette opět automaticky zvolí správné operátory: ```php -$table->where('id', []); // id IS NULL AND FALSE -$table->where('id NOT', []); // id IS NULL OR TRUE -$table->where('NOT (id ?)', $ids); // NOT (id IS NULL AND FALSE) +// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) +$table->where([ + 'status' => 'active', + 'id' => [1, 2, 3], +]); +``` + +V poli můžeme kombinovat SQL výrazy se zástupnými otazníky a více parametry. To je vhodné pro komplexní podmínky s přesně definovanými operátory: -// toto způsobí výjimku, tato syntax není podporovaná -$table->where('NOT id ?', $ids); +```php +// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // dva parametry předáme jako pole +]); ``` +Vícenásobné volání `where()` podmínky automaticky spojuje pomocí AND. -whereOr() ---------- -Příklad použití bez parametrů: +whereOr(array $parameters): static .[method] +-------------------------------------------- + +Podobně jako `where()` přidává podmínky, ale s tím rozdílem, že je spojuje pomocí OR: ```php -// WHERE (user_id IS NULL) OR (SUM(`field1`) > SUM(`field2`)) +// WHERE (`status` = 'active') OR (`deleted` = 1) $table->whereOr([ - 'user_id IS NULL', - 'SUM(field1) > SUM(field2)', + 'status' => 'active', + 'deleted' => true, ]); ``` -Použijeme parametry. Pokud neuvedeme operátor, Nette Database Explorer automaticky přidá vhodný: +I zde můžeme použít komplexnější výrazy: ```php -// WHERE (`field1` IS NULL) OR (`field2` IN (3, 5)) OR (`amount` > 11) +// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) $table->whereOr([ - 'field1' => null, - 'field2' => [3, 5], - 'amount >' => 11, + 'price > ?' => 1000, + 'price_with_tax > ?' => 1500, ]); ``` -V klíči lze uvést výraz obsahující zástupné otazníky a v hodnotě pak předáme parametry: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Přidá podmínku pro primární klíč tabulky: ```php -// WHERE (`id` > 12) OR (ROUND(`id`, 5) = 3) -$table->whereOr([ - 'id > ?' => 12, - 'ROUND(id, ?) = ?' => [5, 3], -]); +// WHERE `id` = 123 +$table->wherePrimary(123); + +// WHERE `id` IN (1, 2, 3) +$table->wherePrimary([1, 2, 3]); +``` + +Pokud má tabulka kompozitní primární klíč (např. `foo_id`, `bar_id`), předáme jej jako pole: + +```php +// WHERE `foo_id` = 1 AND `bar_id` = 5 +$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); + +// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) +$table->wherePrimary([ + ['foo_id' => 1, 'bar_id' => 5], + ['foo_id' => 2, 'bar_id' => 3], +])->fetchAll(); ``` -order() -------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Příklady použití: +Určuje pořadí, v jakém budou řádky vráceny. Můžeme řadit podle jednoho či více sloupců, v sestupném či vzestupném pořadí, nebo podle vlastního výrazu: ```php -$table->order('field1'); // ORDER BY `field1` -$table->order('field1 DESC, field2'); // ORDER BY `field1` DESC, `field2` -$table->order('field = ? DESC', 123); // ORDER BY `field` = 123 DESC +$table->order('created'); // ORDER BY `created` +$table->order('created DESC'); // ORDER BY `created` DESC +$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` +$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC ``` -select() --------- +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- -Příklady použití: +Specifikuje sloupce, které se mají vrátit z databáze. Ve výchozím stavu Nette Database Explorer vrací pouze ty sloupce, které se reálně použijí v kódu. Metodu `select()` tak používáme v případech, kdy potřebujeme vrátit specifické výrazy: ```php -$table->select('field1'); // SELECT `field1` -$table->select('col, UPPER(col) AS abc'); // SELECT `col`, UPPER(`col`) AS abc -$table->select('SUBSTR(title, ?)', 3); // SELECT SUBSTR(`title`, 3) +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Aliasy definované pomocí `AS` jsou pak dostupné jako vlastnosti objektu ActiveRow: + +```php +foreach ($table as $row) { + echo $row->formatted_date; // přístup k aliasu +} +``` -limit() -------- -Příklady použití: +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +Omezuje počet vrácených řádků (LIMIT) a volitelně umožňuje nastavit offset: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (vrátí prvních 10 řádků) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +Pro stránkování je vhodnější použít metodu `page()`. + -page() ------- +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- -Alternativní způsob pro nastavení limitu a offsetu: +Usnadňuje stránkování výsledků. Přijímá číslo stránky (počítané od 1) a počet položek na stránku. Volitelně lze předat referenci na proměnnou, do které se uloží celkový počet stránek: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Celkem stránek: $numOfPages"; ``` -Získání čísla poslední stránky, předá se do proměnné `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Seskupuje řádky podle zadaných sloupců (GROUP BY). Používá se obvykle ve spojení s agregačními funkcemi: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Spočítá počet produktů v každé kategorii +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() -------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Příklady použití: +Nastavuje podmínku pro filtrování seskupených řádků (HAVING). Lze ji použít ve spojení s metodou `group()` a agregačními funkcemi: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Nalezne kategorie, které mají více než 100 produktů +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() --------- +Čtení dat +========= + +Pro čtení dat z databáze máme k dispozici několik užitečných metod: + +.[language-php] +| `foreach ($table as $key => $row)` | Iteruje přes všechny řádky, `$key` je hodnota primárního klíče, `$row` je objekt ActiveRow +| `$row = $table->get($key)` | Vrátí jeden řádek podle primárního klíče +| `$row = $table->fetch()` | Vrátí aktuální řádek a posune ukazatel na další +| `$array = $table->fetchPairs()` | Vytvoří asociativní pole z výsledků +| `$array = $table->fetchAll()` | Vráti všechny řádky jako pole +| `count($table)` | Vrátí počet řádků v objektu Selection + +Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] je určen pouze pro čtení. To znamená, že nelze měnit hodnoty jeho properties. Toto omezení zajišťuje konzistenci dat a zabraňuje neočekávaným vedlejším efektům. Data se načítají z databáze a jakákoliv změna by měla být provedena explicitně a kontrolovaně. + -Příklady použití: +`foreach` - iterace přes všechny řádky +-------------------------------------- + +Nejsnazší způsob, jak vykonat dotaz a získat řádky, je iterováním v cyklu `foreach`. Automaticky spouští SQL dotaz. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key je hodnota primárního klíče, $book je ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Výběry hodnotou z jiné tabulky .[#toc-joining-key] --------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Vykoná SQL dotaz a vrátí řádek podle primárního klíče, nebo `null`, pokud neexistuje. -Často potřebujeme filtrovat výsledky pomocí podmínky, která zahrnuje jinou databázovou tabulku. Tento typ podmínek vyžaduje spojení tabulek, s Nette Database Explorer už je ale nikdy nemusíme psát ručně. +```php +$book = $explorer->table('book')->get(123); // vrátí ActiveRow s ID 123 nebo null +if ($book) { + echo $book->title; +} +``` -Řekněme, že chceme vybrat všechny knihy, které napsal autor jménem `Jon`. Musíme napsat pouze jméno spojovacího klíče relace a název sloupce spojené tabulky. Spojovací klíč je odvozen od jména sloupce, který odkazuje na tabulku, se kterou se chceme spojit. V našem příkladu (viz databázové schéma) je to sloupec `author_id`, ze kterého stačí použít část - `author`. `name` je název sloupce v tabulce `author`. Můžeme vytvořit podmínku také pro překladatele knihy, který je připojen sloupcem `translator_id`. + +fetch(): ?ActiveRow .[method] +----------------------------- + +Vrací řádek a posune interní ukazatel na další. Pokud už neexistují další řádky, vrací `null`. ```php $books = $explorer->table('book'); -$books->where('author.name LIKE ?', '%Jon%'); -$books->where('translator.name', 'David Grudl'); +while ($book = $books->fetch()) { + $this->processBook($book); +} ``` -Logika vytváření spojovacího klíče je dána implementací [Conventions |api:Nette\Database\Conventions]. Doporučujeme použití [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], které analyzuje cizí klíče a umožňuje jednoduše pracovat se vztahy mezi tabulkami. -Vztah mezi knihou a autorem je 1:N. Obrácený vztah je také možný, nazýváme ho **backjoin**. Podívejme se na následující příklad. Chceme vybrat všechny autory, kteří napsali více než tři knihy. Pro vytvoření obráceného spojení použijeme `:` (dvojtečku). Dvojtečka znamená, že jde o vztah hasMany (a je to logické, dvě tečky jsou více než jedna). Bohužel třída Selection není dostatečně chytrá a musíme mu pomoci s agregací výsledků a předat mu část `GROUP BY`, také podmínka musí být zapsaná jako `HAVING`. +fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] +--------------------------------------------------------------------------------------- + +Vrátí výsledky jako asociativní pole. První argument určuje název sloupce, který se použije jako klíč v poli, druhý argument určuje název sloupce, který se použije jako hodnota: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book.id) > 3'); +$authors = $explorer->table('author')->fetchPairs('id', 'name'); +// [1 => 'John Doe', 2 => 'Jane Doe', ...] +``` + +Pokud uvedeme pouze první parametr, bude hodnotou celý řadek, tedy objekt `ActiveRow`: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + +V případě duplicitních klíčů se použije hodnota z posledního řádku. Při použití `null` jako klíče bude pole indexováno numericky od nuly (pak ke kolizím nedochází): + +```php +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] ``` -Možná jste si všimli, že spojovací výraz odkazuje na `book`, ale není jasné, jestli spojujeme přes `author_id` nebo `translator_id`. Ve výše uvedeném příkladu Selection spojuje přes sloupec `author_id`, protože byla nalezena shoda se jménem zdrojové tabulky - tabulky `author`. Pokud by neexistovala shoda a existovalo více možností, Nette vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. -Abychom mohli spojovat přes `translator_id`, stačí přidat volitelný parametr do spojovacího výrazu. +fetchPairs(Closure $callback): array .[method] +---------------------------------------------- + +Alternativně můžete jako parametr uvést callback, který bude pro každý řádek vracet buď samotnou hodnotu, nebo dvojici klíč-hodnota. ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book(translator).id) > 3'); +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// ['První kniha (Jan Novák)', ...] + +// Callback může také vracet pole s dvojicí klíč & hodnota: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['První kniha' => 'Jan Novák', ...] ``` -Teď se podívejme na složitější příklad na skládání tabulek. -Chceme vybrat všechny autory, kteří napsali něco o PHP. Všechny knihy mají štítky, takže chceme vybrat všechny autory, kteří napsali knihu se štítkem 'PHP'. +fetchAll(): array .[method] +--------------------------- + +Vrátí všechny řádky jako asociativní pole objektů `ActiveRow`, kde klíče jsou hodnoty primárních klíčů. ```php -$authors = $explorer->table('author'); -$authors->where(':book:book_tags.tag.name', 'PHP') - ->group('author.id') - ->having('COUNT(:book:book_tags.tag.id) > 0'); +$allBooks = $explorer->table('book')->fetchAll(); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -Agregace výsledků ------------------ +count(): int .[method] +---------------------- -| `$table->count('*')` | Vrátí počet řádků -| `$table->count("DISTINCT $column")` | Vrátí počet odlišných hodnot -| `$table->min($column)` | Vrátí minimální hodnotu -| `$table->max($column)` | Vrátí maximální hodnotu -| `$table->sum($column)` | Vrátí součet všech hodnot -| `$table->aggregation("GROUP_CONCAT($column)")` | Pro jakoukoliv jinou agregační funkci +Metoda `count()` bez parametru vrací počet řádků v objektu `Selection`: -.[caution] -Metoda `count()` bez uvedeného parametru vybere všechny záznamy a vrátí velikost pole, což je velmi neefektivní. Pokud potřebujete například spočítat počet řádků pro stránkování, vždy první argument uveďte. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternativa +``` +Pozor, `count()` s parametrem provádí agregační funkci COUNT v databázi, viz níže. -Escapování a uvozovky -===================== -Database Explorer umí chytře escapovat parametry a identifikátory. Pro správnou funkčnost je ale nutno dodržovat několik pravidel: +ActiveRow::toArray(): array .[method] +------------------------------------- -- klíčová slova, názvy funkcí, procedur apod. psát velkými písmeny -- názvy sloupečků a tabulek psát malými písmeny -- hodnoty dosazovat přes parametry +Převede objekt `ActiveRow` na asociativní pole, kde klíče jsou názvy sloupců a hodnoty jsou odpovídající data. ```php -->where('name like ?', 'John'); // ŠPATNĚ! vygeneruje: `name` `like` ? -->where('name LIKE ?', 'John'); // SPRÁVNĚ +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray bude ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + + +Agregace +======== + +Třída `Selection` poskytuje metody pro snadné provádění agregačních funkcí (COUNT, SUM, MIN, MAX, AVG atd.). + +.[language-php] +| `count($expr)` | Spočítá počet řádků +| `min($expr)` | Vrátí minimální hodnotu ve sloupci +| `max($expr)` | Vrátí maximální hodnotu ve sloupci +| `sum($expr)` | Vrátí součet hodnot ve sloupci +| `aggregation($function)` | Umožňuje provést libovolnou agregační funkci. Např. `AVG()`, `GROUP_CONCAT()` -->where('KEY = ?', $value); // ŠPATNĚ! KEY je klíčové slovo -->where('key = ?', $value); // SPRÁVNĚ. vygeneruje: `key` = ? -->where('name = ' . $name); // ŠPATNĚ! sql injection! -->where('name = ?', $name); // SPRÁVNĚ +count(string $expr): int .[method] +---------------------------------- -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // ŠPATNĚ! hodnoty dosazujeme přes parametr -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // SPRÁVNĚ +Provede SQL dotaz s funkcí COUNT a vrátí výsledek. Metoda se používá k zjištění, kolik řádků odpovídá určité podmínce: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` ``` -.[warning] -Špatné použití může vést k bezpečnostním dírám v aplikaci. +Pozor, [#count()] bez parametru pouze vrací počet řádků v objektu `Selection`. -Čtení dat -========= +min(string $expr) a max(string $expr) .[method] +----------------------------------------------- + +Metody `min()` a `max()` vrací minimální a maximální hodnotu ve specifikovaném sloupci nebo výrazu: + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr) .[method] +--------------------------- + +Vrací součet hodnot ve specifikovaném sloupci nebo výrazu: + +```php +// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); +``` + + +aggregation(string $function, ?string $groupFunction = null) .[method] +---------------------------------------------------------------------- + +Umožňuje provést libovolnou agregační funkci. + +```php +// průměrná cena produktů v kategorii +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// spojí štítky produktu do jednoho řetězce +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Pokud potřebujeme agregovat výsledky, které už samy o sobě vzešly z nějaké agregační funkce a seskupení (např. `SUM(hodnota)` přes seskupené řádky), jako druhý argument uvedeme agregační funkci, která se má na tyto mezivýsledky aplikovat: + +```php +// Vypočítá celkovou cenu produktů na skladě pro jednotlivé kategorie a poté sečte tyto ceny dohromady. +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` -| `foreach ($table as $id => $row)` | Iteruje přes všechny řádky výsledku -| `$row = $table->get($id)` | Vrátí jeden řádek s ID $id -| `$row = $table->fetch()` | Vrátí další řádek výsledku -| `$array = $table->fetchPairs($key, $value)` | Vrátí všechny výsledky jako asociativní pole -| `$array = $table->fetchPairs($key)` | Vrátí všechny řádky jako asociativní pole -| `count($table)` | Vrátí počet řádků výsledku +V tomto příkladu nejprve vypočítáme celkovou cenu produktů v každé kategorii (`SUM(price * stock) AS category_total`) a seskupíme výsledky podle `category_id`. Poté použijeme `aggregation('SUM(category_total)', 'SUM')` k sečtení těchto mezisoučtů `category_total`. Druhý argument `'SUM'` říká, že se má na mezivýsledky aplikovat funkce SUM. Insert, Update & Delete ======================= -Metoda `insert()` přijímá pole nebo Traversable objekty (například [ArrayHash |utils:arrays#ArrayHash] se kterým pracují [formuláře |forms:]): +Nette Database Explorer zjednodušuje vkládání, aktualizaci a mazání dat. Všechny uvedené metody v případě vyhodí výjimku `Nette\Database\DriverException`. + + +Selection::insert(iterable $data) .[method] +------------------------------------------- + +Vloží nové záznamy do tabulky. + +**Vkládání jednoho záznamu:** + +Nový záznam předáme jako asociativní pole nebo iterable objekt (například ArrayHash používaný ve [formulářích |forms:]), kde klíče odpovídají názvům sloupců v tabulce. + +Pokud má tabulka definovaný primární klíč, metoda vrací objekt `ActiveRow`, který se znovunačte z databáze, aby se zohlednily případné změny provedené na úrovni databáze (triggery, výchozí hodnoty sloupců, výpočty auto-increment sloupců). Tím je zajištěna konzistence dat a objekt vždy obsahuje aktuální data z databáze. Pokud jednoznačný primární klíč nemá, vrací předaná data ve formě pole. ```php $row = $explorer->table('users')->insert([ - 'name' => $name, - 'year' => $year, + 'name' => 'John Doe', + 'email' => 'john.doe@example.com', ]); -// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978) +// $row je instance ActiveRow a obsahuje kompletní data vloženého řádku, +// včetně automaticky generovaného ID a případných změn provedených triggery +echo $row->id; // Vypíše ID nově vloženého uživatele +echo $row->created_at; // Vypíše čas vytvoření, pokud je nastaven triggerem ``` -Má-li tabulka definovaný primární klíč, vrací nový řádek jako objekt ActiveRow. +**Vkládání více záznamů najednou:** -Vícenásobný insert: +Metoda `insert()` umožňuje vložit více záznamů pomocí jednoho SQL dotazu. V tomto případě vrací počet vložených řádků. ```php -$explorer->table('users')->insert([ +$insertedRows = $explorer->table('users')->insert([ + [ + 'name' => 'John', + 'year' => 1994, + ], [ - 'name' => 'Jim', - 'year' => 1978, - ], [ 'name' => 'Jack', - 'year' => 1987, - ] + 'year' => 1995, + ], ]); -// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978), ('Jack', 1987) +// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) +// $insertedRows bude 2 +``` + +Jako parametr lze také předat objekt `Selection` s výběrem dat. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); ``` -Jako parametry můžeme předávat i soubory nebo objekty DateTime: +**Vkládání speciálních hodnot:** + +Jako hodnoty můžeme předávat i soubory, objekty DateTime nebo SQL literály: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // nebo $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // vloží soubor + 'name' => 'John', + 'created_at' => new DateTime, // převede na databázový formát + 'avatar' => fopen('image.jpg', 'rb'), // vloží binární obsah souboru + 'uuid' => $explorer::literal('UUID()'), // zavolá funkci UUID() ]); ``` -Úprava záznamů (vrací počet změněných řádků): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Aktualizuje řádky v tabulce podle zadaného filtru. Vrací počet skutečně změněných řádků. + +Měněné sloupce předáme jako asociativní pole nebo iterable objekt (například ArrayHash používaný ve [formulářích |forms:]), kde klíče odpovídají názvům sloupců v tabulce: ```php -$count = $explorer->table('users') - ->where('id', 10) // musí se volat před update() +$affected = $explorer->table('users') + ->where('id', 10) ->update([ - 'name' => 'Ned Stark' + 'name' => 'John Smith', + 'year' => 1994, ]); -// UPDATE `users` SET `name`='Ned Stark' WHERE (`id` = 10) +// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 ``` -Pro update můžeme využít operátorů `+=` a `-=`: +Pro změnu číselných hodnot můžeme použít operátory `+=` a `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // všimněte si += + 'points+=' => 1, // zvýší hodnotu sloupce 'points' o 1 + 'coins-=' => 1, // sníží hodnotu sloupce 'coins' o 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Mazání záznamů (vrací počet smazaných řádků): + +Selection::delete(): int .[method] +---------------------------------- + +Maže řádky z tabulky podle zadaného filtru. Vrací počet smazaných řádků. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +Při volání `update()` a `delete()` nezapomeňte pomocí `where()` specifikovat řádky, které se mají upravit/smazat. Pokud `where()` nepoužijete, operace se provede na celé tabulce! -Vazby mezi tabulkami -==================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- -Relace Has one --------------- -Relace has one je velmi běžná. Kniha *má jednoho* autora. Kniha *má jednoho* překladatele. Řádek, který je ve vztahu has one získáme pomocí metody `ref()`. Ta přijímá dva argumenty: jméno cílové tabulky a název spojovacího sloupce. Viz příklad: +Aktualizuje data v databázovém řádku reprezentovaném objektem `ActiveRow`. Jako parametr přijímá iterable s daty, která se mají aktualizovat (klíče jsou názvy sloupců). Pro změnu číselných hodnot můžeme použít operátory `+=` a `-=`: + +Po provedení aktualizace se `ActiveRow` automaticky znovu načte z databáze, aby se zohlednily případné změny provedené na úrovni databáze (např. triggery). Metoda vrací true pouze pokud došlo ke skutečné změně dat. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // zvýšíme počet zobrazení +]); +echo $article->views; // Vypíše aktuální počet zobrazení ``` -V příkladu výše vybíráme souvisejícího autora z tabulky `author`. Primární klíč tabulky `author` je hledán podle sloupce `book.author_id`. Metoda `ref()` vrací instanci `ActiveRow` nebo `null`, pokud hledaný záznam neexistuje. Vrácený řádek je instance `ActiveRow`, takže s ním můžeme pracovat stejně jako se záznamem knihy. +Tato metoda aktualizuje pouze jeden konkrétní řádek v databázi. Pro hromadnou aktualizaci více řádků použijte metodu [#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Smaže řádek z databáze, který je reprezentován objektem `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Smaže knihu s ID 1 +``` + +Tato metoda maže pouze jeden konkrétní řádek v databázi. Pro hromadné smazání více řádků použijte metodu [#Selection::delete()]. + + +Vazby mezi tabulkami +==================== + +V relačních databázích jsou data rozdělena do více tabulek a navzájem propojená pomocí cizích klíčů. Nette Database Explorer přináší revoluční způsob, jak s těmito vazbami pracovat - bez psaní JOIN dotazů a nutnosti cokoliv konfigurovat nebo generovat. + +Pro ilustraci práce s vazbami použijeme příklad databáze knih ([najdete jej na GitHubu |https://github.com/nette-examples/books]). V databázi máme tabulky: + +- `author` - spisovatelé a překladatelé (sloupce `id`, `name`, `web`, `born`) +- `book` - knihy (sloupce `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - štítky (sloupce `id`, `name`) +- `book_tag` - vazební tabulka mezi knihami a štítky (sloupce `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Struktura databáze .<> + +V našem příkladu databáze knih najdeme několik typů vztahů (byť model je zjednodušený oproti realitě): -// nebo přímo -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +- One-to-many 1:N – každá kniha **má jednoho** autora, autor může napsat **několik** knih +- Zero-to-many 0:N – kniha **může mít** překladatele, překladatel může přeložit **několik** knih +- Zero-to-one 0:1 – kniha **může mít** další díl +- Many-to-many M:N – kniha **může mít několik** tagů a tag může být přiřazen **několika** knihám + +V těchto vztazích vždy existuje tabulka nadřazená a podřízená. Například ve vztahu mezi autorem a knihou je tabulka `author` nadřazená a `book` podřízená - můžeme si to představit tak, že kniha vždy "patří" nějakému autorovi. To se projevuje i ve struktuře databáze: podřízená tabulka `book` obsahuje cizí klíč `author_id`, který odkazuje na nadřazenou tabulku `author`. + +Potřebujeme-li vypsat knihy včetně jmen jejich autorů, máme dvě možnosti. Buď data získáme jediným SQL dotazem pomocí JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id +``` + +Nebo načteme data ve dvou krocích - nejprve knihy a pak jejich autory - a potom je v PHP poskládáme: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- ids autorů získaných knih ``` -Kniha má také jednoho překladatele, jeho jméno získáme snadno. +Druhý přístup je ve skutečnosti efektivnější, i když to může být překvapivé. Data jsou načtena pouze jednou a mohou být lépe využita v cache. Právě tímto způsobem pracuje Nette Database Explorer - vše řeší pod povrchem a vám nabízí elegantní API: + ```php -$book->ref('author', 'translator_id')->name +$books = $explorer->table('book'); +foreach ($books as $book) { + echo 'title: ' . $book->title; + echo 'written by: ' . $book->author->name; // $book->author je záznam z tabulky 'author' + echo 'translated by: ' . $book->translator?->name; +} ``` -Tento přístup je funkční, ale pořád trochu zbytečně těžkopádný, nemyslíte? Databáze už obsahuje definice cizích klíčů, tak proč je nepoužít automaticky. Pojďme to vyzkoušet. -Pokud přistoupíme k členské proměnné, která neexistuje, ActiveRow se pokusí použít jméno této proměnné pro relaci 'has one'. Čtení této proměnné je stejné jako volání metody `ref()` pouze s jedním parametrem. Tomuto parametru budeme říkat **klíč**. Tento klíč bude použit pro vyhledání cizího klíče v tabulce. Předaný klíč je porovnán se sloupci, a pokud odpovídá pravidlům, je cizí klíč na daném sloupci použit pro čtení dat z příbuzné tabulky. Viz příklad: +Přístup k nadřazené tabulce +--------------------------- + +Přístup k nadřazené tabulce je přímočarý. Jde o vztahy jako *kniha má autora* nebo *kniha může mít překladatele*. Související záznam získáme přes property objektu ActiveRow - její název odpovídá názvu sloupce s cizím klíčem bez `id`: ```php -$book->author->name; -// je stejné jako -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // najde autora podle sloupce author_id +echo $book->translator?->name; // najde překladatele podle translator_id ``` -Instance ActiveRow nemá žádný sloupec `author`. Všechny sloupce tabulky `book` jsou prohledány na shodu s *klíčem*. Shoda v tomto případě znamená, že jméno sloupce musí obsahovat klíč. V příkladu výše sloupec `author_id` obsahuje řetězec 'author' a tedy odpovídá klíči 'author'. Pokud chceme přistoupit k záznamu překladatele, obdobným způsobem použijeme klíč 'translator', protože bude odpovídat sloupci `translator_id`. Více o logice párování klíčů si můžete přečíst v části [Joining expressions |#joining-key]. +Když přistoupíme k property `$book->author`, Explorer v tabulce `book` hledá sloupec, jehož název obsahuje řetězec `author` (tedy `author_id`). Podle hodnoty v tomto sloupci načte odpovídající záznam z tabulky `author` a vrátí jej jako `ActiveRow`. Podobně funguje i `$book->translator`, který využije sloupec `translator_id`. Protože sloupec `translator_id` může obsahovat `null`, použijeme v kódu operátor `?->`. + +Alternativní cestu nabízí metoda `ref()`, která přijímá dva argumenty, název cílové tabulky a název spojovacího sloupce, a vrací instanci `ActiveRow` nebo `null`: ```php -echo $book->title . ': '; -echo $book->author->name; -if ($book->translator) { - echo ' (translated by ' . $book->translator->name . ')'; -} +echo $book->ref('author', 'author_id')->name; // vazba na autora +echo $book->ref('author', 'translator_id')->name; // vazba na překladatele ``` -Pokud chceme získat autora více knih, použijeme stejný přístup. Nette Database Explorer za nás z databáze záznamy autorů a překladatelů pro všechny knihy najednou. +Metoda `ref()` se hodí, pokud nelze použít přístup přes property, protože tabulka obsahuje sloupec se stejným názvem (tj. `author`). V ostatních případech je doporučeno používat přístup přes property, který je čitelnější. + +Explorer automaticky optimalizuje databázové dotazy. Když procházíme knihy v cyklu a přistupujeme k jejich souvisejícím záznamům (autorům, překladatelům), Explorer negeneruje dotaz pro každou knihu zvlášť. Místo toho provede pouze jeden SELECT pro každý typ vazby, čímž výrazně snižuje zátěž databáze. Například: ```php $books = $explorer->table('book'); foreach ($books as $book) { echo $book->title . ': '; echo $book->author->name; - if ($book->translator) { - echo ' (translated by ' . $book->translator->name . ')'; - } + echo $book->translator?->name; } ``` -Tento kód zavolá pouze tyto tři dotazy do databáze: +Tento kód zavolá pouze tyto tři bleskové dotazy do databáze: + ```sql SELECT * FROM `book`; SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id ze sloupce author_id vybraných knih SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id ze sloupce translator_id vybraných knih ``` +.[note] +Logika dohledávání spojovacího sloupce je dána implementací [Conventions |api:Nette\Database\Conventions]. Doporučujeme použití [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], které analyzuje cizí klíče a umožňuje jednoduše pracovat s existujícími vztahy mezi tabulkami. -Relace Has many ---------------- -Relace 'has many' je pouze obrácená 'has one' relace. Autor napsal několik (*many*) knih. Autor přeložil několik (*many*) knih. Tento typ relace je obtížnější, protože vztah je pojmenovaný ('napsal', 'přeložil'). ActiveRow má metodu `related()`, která vrací pole souvisejících záznamů. Záznamy jsou opět instance ActiveRow. Viz příklad: +Přístup k podřízené tabulce +--------------------------- + +Přístup k podřízené tabulce funguje v opačném směru. Nyní se ptáme *jaké knihy napsal tento autor* nebo *přeložil tento překladatel*. Pro tento typ dotazu používáme metodu `related()`, která vrátí `Selection` se souvisejícími záznamy. Podívejme se na příklad: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' napsal:'; +$author = $explorer->table('author')->get(1); +// Vypíše všechny knihy od autora foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Napsal: $book->title"; } -echo 'a přeložil:'; +// Vypíše všechny knihy, které autor přeložil foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Přeložil: $book->title"; } ``` -Metoda `related()` přijímá popis spojení jako dva argumenty, nebo jako jeden argument spojený tečkou. První argument je cílová tabulka, druhý je sloupec. +Metoda `related()` přijímá popis spojení jako jeden argument s tečkovou notací nebo jako dva samostatné argumenty: ```php -$author->related('book.translator_id'); -// je stejné jako -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // jeden argument +$author->related('book', 'translator_id'); // dva argumenty ``` -Můžeme použít heuristiku Nette Database Explorer založenou na cizích klíčích a použít pouze **klíč**. Klíč bude porovnán s cizími klíči, které odkazují do aktuální tabulky (tabulka `author`). Pokud je nalezena shoda, Nette Database Explorer použije tento cizí klíč, v opačném případě vyhodí výjimku [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] nebo [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Více o logice párování klíčů si můžete přečíst v části [Joining expressions |#joining-key]. +Explorer dokáže automaticky detekovat správný spojovací sloupec na základě názvu nadřazené tabulky. V tomto případě se spojuje přes sloupec `book.author_id`, protože název zdrojové tabulky je `author`: -Metodu `related()` může samozřejmě volat na všechny získané autory a Nette Database Explorer načte všechny odpovídající knihy najednou. +```php +$author->related('book'); // použije book.author_id +``` + +Pokud by existovalo více možných spojení, Explorer vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Metodu `related()` můžeme samozřejmě použít i při procházení více záznamů v cyklu a Explorer i v tomto případě automaticky optimalizuje dotazy: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { echo $author->name . ' napsal:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -Příklad uvedený výše spustí pouze tyto dva dotazy do databáze: +Tento kód vygeneruje pouze dva bleskové SQL dotazy: ```sql SELECT * FROM `author`; @@ -533,18 +802,111 @@ SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id vybraných autorů ``` -Ruční vytvoření Explorer -======================== +Vazba Many-to-many +------------------ + +Pro vazbu many-to-many (M:N) je potřeba existence vazební tabulky (v našem případě `book_tag`), která obsahuje dva sloupce s cizími klíči (`book_id`, `tag_id`). Každý z těchto sloupců odkazuje na primární klíč jedné z propojovaných tabulek. Pro získání souvisejících dat nejprve získáme záznamy z vazební tabulky pomocí `related('book_tag')` a dále pokračujeme k cílovým datům: + +```php +$book = $explorer->table('book')->get(1); +// vypíše názvy tagů přiřazených ke knize +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // vypíše název tagu přes vazební tabulku +} + +$tag = $explorer->table('tag')->get(1); +// nebo opačně: vypíše názvy knih označených tímto tagem +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // vypíše název knihy +} +``` + +Explorer opět optimalizuje SQL dotazy do efektivní podoby: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id vybraných knih +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id tagů nalezených v book_tag +``` + -Pokud jsme si vytvořili databázové spojení pomocí aplikační konfigurace, nemusíme se o nic starat. Vytvořila se nám totiž i služba typu `Nette\Database\Explorer`, kterou si můžeme předat pomocí DI. +Dotazování přes související tabulky +----------------------------------- -Pokud ale používáme Nette Database Explorer samostatně, musíme instanci `Nette\Database\Explorer` vytvořit ručně. +V metodách `where()`, `select()`, `order()` a `group()` můžeme používat speciální notace pro přístup k sloupcům z jiných tabulek. Explorer automaticky vytvoří potřebné JOINy. + +**Tečková notace** (`nadřazená_tabulka.sloupec`) se používá pro vztah 1:N z pohledu podřízené tabulky: ```php -// $storage obsahuje implementaci Nette\Caching\Storage, např.: -$storage = new Nette\Caching\Storages\FileStorage($tempDir); -$connection = new Nette\Database\Connection($dsn, $user, $password); -$structure = new Nette\Database\Structure($connection, $storage); -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); +$books = $explorer->table('book'); + +// Najde knihy, jejichž autor má jméno začínající na 'Jon' +$books->where('author.name LIKE ?', 'Jon%'); + +// Seřadí knihy podle jména autora sestupně +$books->order('author.name DESC'); + +// Vypíše název knihy a jméno autora +$books->select('book.title, author.name'); ``` + +**Dvojtečková notace** (`:podřízená_tabulka.sloupec`) se používá pro vztah 1:N z pohledu nadřazené tabulky: + +```php +$authors = $explorer->table('author'); + +// Najde autory, kteří napsali knihu s 'PHP' v názvu +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Spočítá počet knih pro každého autora +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +Ve výše uvedeném příkladu s dvojtečkovou notací (`:book.title`) není specifikován sloupec s cizím klíčem. Explorer automaticky detekuje správný sloupec na základě názvu nadřazené tabulky. V tomto případě se spojuje přes sloupec `book.author_id`, protože název zdrojové tabulky je `author`. Pokud by existovalo více možných spojení, Explorer vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Spojovací sloupec lze explicitně uvést v závorce: + +```php +// Najde autory, kteří přeložili knihu s 'PHP' v názvu +$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); +``` + +Notace lze řetězit pro přístup přes více tabulek: + +```php +// Najde autory knih označených tagem 'PHP' +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Rozšíření podmínek pro JOIN +--------------------------- + +Metoda `joinWhere()` rozšiřuje podmínky, které se uvádějí při propojování tabulek v SQL za klíčovým slovem `ON`. + +Dejme tomu, že chceme najít knihy přeložené konkrétním překladatelem: + +```php +// Najde knihy přeložené překladatelem jménem 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +V podmínce `joinWhere()` můžeme používat stejné konstrukce jako v metodě `where()` - operátory, zástupné otazníky, pole hodnot či SQL výrazy. + +Pro složitější dotazy s více JOINy můžeme definovat aliasy tabulek: + +```php +$tags = $explorer->table('tag') + ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) + ->alias(':book_tag.book.author', 'book_author'); +// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` +// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` +// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` +// AND (`book_author`.`born` < 1950) +``` + +Všimněte si, že zatímco metoda `where()` přidává podmínky do klauzule `WHERE`, metoda `joinWhere()` rozšiřuje podmínky v klauzuli `ON` při spojování tabulek. diff --git a/database/cs/guide.texy b/database/cs/guide.texy new file mode 100644 index 0000000000..f70849ed6c --- /dev/null +++ b/database/cs/guide.texy @@ -0,0 +1,216 @@ +Nette Database +************** + +.[perex] +Nette Database je výkonná a elegantní databázová vrstva pro PHP s důrazem na jednoduchost a chytré funkce. Nabízí dva způsoby práce s databází - [Explorer] pro rychlý vývoj aplikací, nebo [SQL přístup |SQL way] pro přímou práci s dotazy. + +<div class="grid gap-3"> +<div> + + +[SQL přístup |SQL way] +====================== +- Bezpečné parametrizované dotazy +- Přesná kontrola nad podobou SQL dotazů +- Když píšete komplexní dotazy s pokročilými funkcemi +- Optimalizujete výkon pomocí specifických SQL funkcí + +</div> + +<div> + + +[Explorer] +========== +- Vyvíjíte rychle bez psaní SQL +- Intuitivní práce s relacemi mezi tabulkami +- Oceníte automatickou optimalizaci dotazů +- Vhodné pro rychlou a pohodlnout práci s databází + +</div> + +</div> + + +Instalace +========= + +Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:composer]: + +```shell +composer require nette/database +``` + + +Podporované databáze +==================== + +Nette Database podporuje následující databáze: + +|* Databázový server |* DSN jméno |* Podpora v Explorer +|---------------------|-------------|----------------------- +| MySQL (>= 5.1) | mysql | ANO +| PostgreSQL (>= 9.0) | pgsql | ANO +| Sqlite 3 (>= 3.8) | sqlite | ANO +| Oracle | oci | - +| MS SQL (PDO_SQLSRV) | sqlsrv | ANO +| MS SQL (PDO_DBLIB) | mssql | - +| ODBC | odbc | - + + +Dva přístupy k databázi +======================= + +Nette Database vám dává na výběr: můžete buď psát SQL dotazy přímo (SQL přístup), nebo je nechat generovat automaticky (Explorer). Podívejme se, jak oba přístupy řeší stejné úkoly: + +[SQL přístup|sql way] - SQL dotazy + +```php +// vložení záznamu +$database->query('INSERT INTO books', [ + 'author_id' => $authorId, + 'title' => $bookData->title, + 'published_at' => new DateTime, +]); + +// získání záznamů: autoři knih +$result = $database->query(' + SELECT authors.*, COUNT(books.id) AS books_count + FROM authors + LEFT JOIN books ON authors.id = books.author_id + WHERE authors.active = 1 + GROUP BY authors.id +'); + +// výpis (není optimální, generuje N dalších dotazů) +foreach ($result as $author) { + $books = $database->query(' + SELECT * FROM books + WHERE author_id = ? + ORDER BY published_at DESC + ', $author->id); + + echo "Autor $author->name napsal $author->books_count knih:\n"; + + foreach ($books as $book) { + echo "- $book->title\n"; + } +} +``` + +[Explorer přístup|explorer] - automatické generování SQL + +```php +// vložení záznamu +$database->table('books')->insert([ + 'author_id' => $authorId, + 'title' => $bookData->title, + 'published_at' => new DateTime, +]); + +// získání záznamů: autoři knih +$authors = $database->table('authors') + ->where('active', 1); + +// výpis (automaticky generuje jen 2 optimalizované dotazy) +foreach ($authors as $author) { + $books = $author->related('books') + ->order('published_at DESC'); + + echo "Autor $author->name napsal {$books->count()} knih:\n"; + + foreach ($books as $book) { + echo "- $book->title\n"; + } +} +``` + +Explorer přístup generuje a optimalizuje SQL dotazy automaticky. V uvedeném příkladu SQL přístup vygeneruje N+1 dotazů (jeden pro autory a pak jeden pro knihy každého autora), zatímco Explorer automaticky optimalizuje dotazy a provede pouze dva - jeden pro autory a jeden pro všechny jejich knihy. + +Oba přístupy lze v aplikaci libovolně kombinovat podle potřeby. + + +Připojení a konfigurace +======================= + +Pro připojení k databázi stačí vytvořit instanci třídy [api:Nette\Database\Connection]: + +```php +$database = new Nette\Database\Connection($dsn, $user, $password); +``` + +Parametr `$dsn` (data source name) je stejný, [jaký používá PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], např. `host=127.0.0.1;dbname=test`. V případě selhání vyhodí výjimku `Nette\Database\ConnectionException`. + +Nicméně šikovnější způsob nabízí [aplikační konfigurace |configuration], kam stačí přidat sekci `database` a vytvoří se potřebné objekty a také databázový panel v [Tracy |tracy:] baru. + +```neon +database: + dsn: 'mysql:host=127.0.0.1;dbname=test' + user: root + password: password +``` + +Poté objekt spojení [získáme jako službu z DI kontejneru |dependency-injection:passing-dependencies], např.: + +```php +class Model +{ + public function __construct( + // nebo Nette\Database\Explorer + private Nette\Database\Connection $database, + ) { + } +} +``` + +Více informací o [konfiguraci databáze|configuration]. + + +Ruční vytvoření Explorer +------------------------ + +Pokud nepoužíváte Nette DI kontejner, můžete instanci `Nette\Database\Explorer` vytvořit ručně: + +```php +// připojení k databázi +$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// úložiště pro cache, implementuje Nette\Caching\Storage, např.: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// stará se o reflexi databázové struktury +$structure = new Nette\Database\Structure($connection, $storage); +// definuje pravidla pro mapování názvů tabulek, sloupců a cizích klíčů +$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); +$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); +``` + + +Správa připojení +================ + +Při vytvoření objektu `Connection` dojde automaticky k připojení. Pokud chcete připojení odložit, použijte lazy režim - ten zapnete v [konfiguraci|configuration] nastavením `lazy`, nebo takto: + +```php +$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); +``` + +Pro správu připojení využijte metody `connect()`, `disconnect()` a `reconnect()`. +- `connect()` vytvoří připojení, pokud ještě neexistuje, přičemž může vyvolat výjimku `Nette\Database\ConnectionException`. +- `disconnect()` odpojí aktuální připojení k databázi. +- `reconnect()` provede odpojení a následné znovu připojení k databázi. Tato metoda může rovněž vyvolat výjimku `Nette\Database\ConnectionException`. + +Kromě toho můžete sledovat události spojené s připojením pomocí události `onConnect`, což je pole callbacků, které se zavolají po navázání spojení s databází. + +```php +// proběhne po připojení k databázi +$database->onConnect[] = function($database) { + echo "Připojeno k databázi"; +}; +``` + + +Tracy Debug Bar +=============== + +Pokud používáte [Tracy |tracy:], aktivuje se automaticky panel Database v Debug baru, který zobrazuje všechny provedené dotazy, jejich parametry, dobu vykonání a místo v kódu, kde byly zavolány. + +[* db-panel.webp *] diff --git a/database/cs/mapping.texy b/database/cs/mapping.texy new file mode 100644 index 0000000000..07bf97e6a9 --- /dev/null +++ b/database/cs/mapping.texy @@ -0,0 +1,55 @@ +Konverze typů +************* + +.[perex] +Nette Database automaticky konvertuje hodnoty vrácené z databáze na odpovídající PHP typy. + + +Datum a čas +----------- + +Časové údaje jsou převáděny na objekty `Nette\Utils\DateTime`. Pokud chcete, aby byly časové údaje převáděny na immutable objekty `Nette\Database\DateTime`, nastavte v [konfiguraci|configuration] volbu `newDateTime` na true. + +```php +$row = $database->fetch('SELECT created_at FROM articles'); +echo $row->created_at instanceof DateTime; // true +echo $row->created_at->format('j. n. Y'); +``` + +V případě MySQL převádí datový typ `TIME` na objekty `DateInterval`. + + +Booleovské hodnoty +------------------ + +Booleovské hodnoty jsou automaticky převedeny na `true` nebo `false`. U MySQL se převádí `TINYINT(1)` pokud nastavíme v [konfiguraci|configuration] `convertBoolean`. + +```php +$row = $database->fetch('SELECT is_published FROM articles'); +echo gettype($row->is_published); // 'boolean' +``` + + +Číselné hodnoty +--------------- + +Číselné hodnoty jsou převedeny na `int` nebo `float` podle typu sloupce v databázi: + +```php +$row = $database->fetch('SELECT id, price FROM products'); +echo gettype($row->id); // integer +echo gettype($row->price); // float +``` + + +Vlastní normalizace +------------------- + +Pomocí metody `setRowNormalizer(?callable $normalizer)` můžete nastavit vlastní funkci pro transformaci řádků z databáze. To se hodí například pro automatický převod datových typů. + +```php +$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { + // tady proběhne konverze typů + return $row; +}); +``` diff --git a/database/cs/reflection.texy b/database/cs/reflection.texy new file mode 100644 index 0000000000..43a83b88d3 --- /dev/null +++ b/database/cs/reflection.texy @@ -0,0 +1,125 @@ +Reflexe struktury +***************** + +.{data-version:3.2.1} +Nette Database poskytuje nástroje pro introspekci databázové struktury pomocí třídy [api:Nette\Database\Reflection]. Ta umožňuje získávat informace o tabulkách, sloupcích, indexech a cizích klíčích. Reflexi můžete využít ke generování schémat, vytváření flexibilních aplikací pracujících s databází nebo obecných databázových nástrojů. + +Objekt reflexe získáme z instance připojení k databázi: + +```php +$reflection = $database->getReflection(); +``` + + +Získání tabulek +--------------- + +Readonly vlastnost `$reflection->tables` obsahuje asociativní pole všech tabulek v databázi: + +```php +// Výpis názvů všech tabulek +foreach ($reflection->tables as $name => $table) { + echo $name . "\n"; +} +``` + +K dispozici jsou ještě dvě metody: + +```php +// Ověření existence tabulky +if ($reflection->hasTable('users')) { + echo "Tabulka users existuje"; +} + +// Vrátí objekt tabulky; pokud neexistuje, vyhodí výjimku +$table = $reflection->getTable('users'); +``` + + +Informace o tabulce +------------------- + +Tabulka je reprezentována objektem [Table|api:Nette\Database\Reflection\Table], který poskytuje následující readonly vlastnosti: + +- `$name: string` – název tabulky +- `$view: bool` – zda se jedná o pohled +- `$fullName: ?string` – plný název tabulky včetně schématu (pokud existuje) +- `$columns: array<string, Column>` – asociativní pole sloupců tabulky +- `$indexes: Index[]` – pole indexů tabulky +- `$primaryKey: ?Index` – primární klíč tabulky nebo null +- `$foreignKeys: ForeignKey[]` – pole cizích klíčů tabulky + + +Sloupce +------- + +Vlastnost `columns` tabulky poskytuje asociativní pole sloupců, kde klíčem je název sloupce a hodnotou instance [Column|api:Nette\Database\Reflection\Column] s těmito vlastnostmi: + +- `$name: string` – název sloupce +- `$table: ?Table` – reference na tabulku sloupce +- `$nativeType: string` – nativní databázový typ +- `$size: ?int` – velikost/délka typu +- `$nullable: bool` – zda může sloupec obsahovat NULL +- `$default: mixed` – výchozí hodnota sloupce +- `$autoIncrement: bool` – zda je sloupec auto-increment +- `$primary: bool` – zda je součástí primárního klíče +- `$vendor: array` – dodatečná metadata specifická pro daný databázový systém + +```php +foreach ($table->columns as $name => $column) { + echo "Sloupec: $name\n"; + echo "Typ: {$column->nativeType}\n"; + echo "Nullable: " . ($column->nullable ? 'Ano' : 'Ne') . "\n"; +} +``` + + +Indexy +------ + +Vlastnost `indexes` tabulky poskytuje pole indexů, kde každý index je instance [Index|api:Nette\Database\Reflection\Index] s těmito vlastnostmi: + +- `$columns: Column[]` – pole sloupců tvořících index +- `$unique: bool` – zda je index unikátní +- `$primary: bool` – zda jde o primární klíč +- `$name: ?string` – název indexu + +Primární klíč tabulky lze získat pomocí vlastnosti `primaryKey`, která vrací buď objekt `Index`, nebo `null` v případě, že tabulka nemá primární klíč. + +```php +// Výpis indexů +foreach ($table->indexes as $index) { + $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); + echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; + echo " Sloupce: $columns\n"; + echo " Unique: " . ($index->unique ? 'Ano' : 'Ne') . "\n"; +} + +// Výpis primárního klíče +if ($primaryKey = $table->primaryKey) { + $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); + echo "Primární klíč: $columns\n"; +} +``` + + +Cizí klíče +---------- + +Vlastnost `foreignKeys` tabulky poskytuje pole cizích klíčů, kde každý cizí klíč je instance [ForeignKey|api:Nette\Database\Reflection\ForeignKey] s těmito vlastnostmi: + +- `$foreignTable: Table` – odkazovaná tabulka +- `$localColumns: Column[]` – pole lokálních sloupců +- `$foreignColumns: Column[]` – pole odkazovaných sloupců +- `$name: ?string` – název cizího klíče + +```php +// Výpis cizích klíčů +foreach ($table->foreignKeys as $fk) { + $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); + $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); + + echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; + echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; +} +``` diff --git a/database/cs/security.texy b/database/cs/security.texy new file mode 100644 index 0000000000..89077c54a2 --- /dev/null +++ b/database/cs/security.texy @@ -0,0 +1,185 @@ +Bezpečnostní rizika +******************* + +<div class=perex> + +Databáze často obsahuje citlivá data a umožňuje provádět nebezpečné operace. Pro bezpečnou práci s Nette Database je klíčové: + +- Porozumět rozdílu mezi bezpečným a nebezpečným API +- Používat parametrizované dotazy +- Správně validovat vstupní data + +</div> + + +Co je SQL Injection? +==================== + +SQL injection je nejzávažnější bezpečnostní riziko při práci s databází. Vzniká, když se neošetřený vstup od uživatele stane součástí SQL dotazu. Útočník může vložit vlastní SQL příkazy a tím: +- Získat neoprávněný přístup k datům +- Modifikovat nebo smazat data v databázi +- Obejít autentizaci + +```php +// ❌ NEBEZPEČNÝ KÓD - zranitelný vůči SQL injection +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Útočník může zadat například hodnotu: ' OR '1'='1 +// Výsledný dotaz pak bude: SELECT * FROM users WHERE name = '' OR '1'='1' +// Což vrátí všechny uživatele +``` + +Totéž se týká i Database Explorer: + +```php +// ❌ NEBEZPEČNÝ KÓD - zranitelný vůči SQL injection +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Parametrizované dotazy +====================== + +Základní obranou proti SQL injection jsou parametrizované dotazy. Nette Database nabízí několik způsobů jejich použití. + +Nejjednodušší způsob je použití **zástupných otazníků**: + +```php +// ✅ Bezpečný parametrizovaný dotaz +$database->query('SELECT * FROM users WHERE name = ?', $name); + +// ✅ Bezpečná podmínka v Exploreru +$table->where('name = ?', $name); +``` + +Tohle platí pro všechny další metody v [Database Explorer|explorer], které umožňují vkládat výrazy se zástupnými otazníky a parametry. + +Pro příkazy INSERT, UPDATE nebo klauzuli WHERE můžeme předat hodnoty v poli: + +```php +// ✅ Bezpečný INSERT +$database->query('INSERT INTO users', [ + 'name' => $name, + 'email' => $email, +]); + +// ✅ Bezpečný INSERT v Exploreru +$table->insert([ + 'name' => $name, + 'email' => $email, +]); +``` + + +Validace hodnot parametrů +========================= + +Parametrizované dotazy jsou základním stavebním kamenem bezpečné práce s databází. Avšak hodnoty, které do nich vkládáme, musí projít několika úrovněmi kontrol: + + +Typová kontrola +--------------- + +**Nejdůležitější je zajistit správný datový typ parametrů** - to je nutná podmínka pro bezpečné použití Nette Database. Databáze předpokládá, že všechna vstupní data mají správný datový typ odpovídající danému sloupci. + +Například pokud by `$name` v předchozích příkladech bylo neočekávaně pole místo řetězce, Nette Database by se pokusilo vložit všechny jeho prvky do SQL dotazu, což by vedlo k chybě. Proto **nikdy nepoužívejte** nevalidovaná data z `$_GET`, `$_POST` nebo `$_COOKIE` přímo v databázových dotazech. + + +Formátová kontrola +------------------ + +Na druhé úrovni kontrolujeme formát dat - například zda jsou řetězce v UTF-8 kódování a jejich délka odpovídá definici sloupce, nebo zda jsou číselné hodnoty v povoleném rozsahu pro daný datový typ sloupce. + +U této úrovně validace se můžeme částečně spolehnout i na databázi samotnou - mnoho databází odmítne nevalidní data. Nicméně chování se může lišit, některé mohou dlouhé řetězce tiše zkrátit nebo čísla mimo rozsah oříznout. + + +Doménová kontrola +----------------- + +Třetí úroveň představují logické kontroly specifické pro vaši aplikaci. Například ověření, že hodnoty ze select boxů odpovídají nabízeným možnostem, že čísla jsou v očekávaném rozsahu (např. věk 0-150 let) nebo že vzájemné závislosti mezi hodnotami dávají smysl. + + +Doporučené způsoby validace +--------------------------- + +- Používejte [Nette Formuláře|forms:], které automaticky zajistí správnou validaci všech vstupů +- Používejte [Presentery|application:] a uvádějte u parametrů v `action*()` a `render*()` metodách datové typy +- Nebo implementujte vlastní validační vrstvu pomocí standardních PHP nástrojů jako `filter_var()` + + +Bezpečná práce se sloupci +========================= + +V předchozí sekci jsme si ukázali, jak správně validovat hodnoty parametrů. Při použití polí v SQL dotazech však musíme věnovat stejnou pozornost i jejich klíčům. + +```php +// ❌ NEBEZPEČNÝ KÓD - nejsou ošetřené klíče v poli +$database->query('INSERT INTO users', $_POST); +``` + +U příkazů INSERT a UPDATE je to zásadní bezpečnostní chyba - útočník může do databáze vložit nebo změnit jakýkoliv sloupec. Mohl by si například nastavit `is_admin = 1` nebo vložit libovolná data do citlivých sloupců (tzv Mass Assignment Vulnerability). + +Ve WHERE podmínkách je to ještě nebezpečnější, protože mohou obsahovat operátory: + +```php +// ❌ NEBEZPEČNÝ KÓD - nejsou ošetřené klíče v poli +$_POST['salary >'] = 100000; +$database->query('SELECT * FROM users WHERE', $_POST); +// vykoná dotaz WHERE (`salary` > 100000) +``` + +Útočník může tento přístup využít k systematickému zjišťování platů zaměstnanců. Začne například dotazem na platy nad 100.000, pak pod 50.000 a postupným zužováním rozsahu může odhalit přibližné platy všech zaměstnanců. Tento typ útoku se nazývá SQL enumeration. + +Metody `where()` a `whereOr()` jsou ještě [mnohem flexibilnější |explorer#where] a podporují v klíčích a hodnotách SQL výrazy včetně operátorů a funkcí. To dává útočníkovi možnost provést SQL injection: + +```php +// ❌ NEBEZPEČNÝ KÓD - útočník může vložit vlastní SQL +$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; +$table->where($_POST); +// vykoná dotaz WHERE (0) UNION SELECT name, salary FROM users WHERE (1) +``` + +Tento útok ukončí původní podmínku pomocí `0)`, připojí vlastní `SELECT` pomocí `UNION` aby získal citlivá data z tabulky `users` a uzavře syntakticky správný dotaz pomocí `WHERE (1)`. + + +Whitelist sloupců +----------------- + +Pro bezpečnou práci s názvy sloupců potřebujeme mechanismus, který zajistí, že uživatel může pracovat pouze s povolenými sloupci a nemůže přidat vlastní. Mohli bychom se pokusit detekovat a blokovat nebezpečné názvy sloupců (blacklist), ale tento přístup je nespolehlivý - útočník může vždy přijít s novým způsobem, jak nebezpečný název sloupce zapsat, který jsme nepředvídali. + +Proto je mnohem bezpečnější obrátit logiku a definovat explicitní seznam povolených sloupců (whitelist): + +```php +// Sloupce, které může uživatel upravovat +$allowedColumns = ['name', 'email', 'active']; + +// Odstraníme všechny nepovolené sloupce ze vstupu +$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); + +// ✅ Nyní můžeme bezpečně použít v dotazech, jako například: +$database->query('INSERT INTO users', $filteredData); +$table->update($filteredData); +$table->where($filteredData); +``` + + +Dynamické identifikátory +======================== + +Pro dynamické názvy tabulek a sloupců použijte zástupný symbol `?name`. Ten zajistí správné escapování identifikátorů podle syntaxe dané databáze (např. pomocí zpětných uvozovek v MySQL): + +```php +// ✅ Bezpečné použití důvěryhodných identifikátorů +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); +// Výsledek v MySQL: SELECT `name` FROM `users` +``` + +Důležité: symbol `?name` používejte pouze pro důvěryhodné hodnoty definované v kódu aplikace. Pro hodnoty od uživatele použijte opět [whitelist |#Whitelist sloupců]. Jinak se vystavujete bezpečnostním rizikům: + +```php +// ❌ NEBEZPEČNÉ - nikdy nepoužívejte vstup od uživatele +$database->query('SELECT ?name FROM users', $_GET['column']); +``` diff --git a/database/cs/sql-way.texy b/database/cs/sql-way.texy new file mode 100644 index 0000000000..5838a7478c --- /dev/null +++ b/database/cs/sql-way.texy @@ -0,0 +1,513 @@ +SQL přístup +*********** + +.[perex] +Nette Database nabízí dvě cesty: můžete psát SQL dotazy sami (SQL přístup), nebo je nechat generovat automaticky (viz [Explorer |explorer]). SQL přístup vám dává plnou kontrolu nad dotazy a přitom zajišťuje jejich bezpečné sestavení. + +.[note] +Detaily k připojení a konfiguraci databáze najdete v kapitole [Připojení a konfigurace |guide#Připojení a konfigurace]. + + +Základní dotazování +=================== + +Pro dotazování do databáze slouží metoda `query()`. Ta vrací objekt [ResultSet |api:Nette\Database\ResultSet], který reprezentuje výsledek dotazu. V případě selhání metoda [vyhodí výjimku|exceptions]. Výsledek dotazu můžeme procházet pomocí cyklu `foreach`, nebo použít některou z [pomocných funkcí |#Získání dat]. + +```php +$result = $database->query('SELECT * FROM users'); + +foreach ($result as $row) { + echo $row->id; + echo $row->name; +} +``` + +Pro bezpečné vkládání hodnot do SQL dotazů používáme parametrizované dotazy. Nette Database je dělá maximálně jednoduché - stačí za SQL dotaz přidat čárku a hodnotu: + +```php +$database->query('SELECT * FROM users WHERE name = ?', $name); +``` + +Při více parametrech máte dvě možnosti zápisu. Buď můžete SQL dotaz "prokládat" parametry: + +```php +$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); +``` + +Nebo napsat nejdříve celý SQL dotaz a pak připojit všechny parametry: + +```php +$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); +``` + + +Ochrana před SQL injection +========================== + +Proč je důležité používat parametrizované dotazy? Protože vás chrání před útokem zvaným SQL injection, při kterém by útočník mohl podstrčit vlastní SQL příkazy a tím získat nebo poškodit data v databázi. + +.[warning] +**Nikdy nevkládejte proměnné přímo do SQL dotazu!** Vždy používejte parametrizované dotazy, které vás ochrání před SQL injection. + +```php +// ❌ NEBEZPEČNÝ KÓD - zranitelný vůči SQL injection +$database->query("SELECT * FROM users WHERE name = '$name'"); + +// ✅ Bezpečný parametrizovaný dotaz +$database->query('SELECT * FROM users WHERE name = ?', $name); +``` + +Seznamte se s [možnými bezpečnostními riziky |security]. + + +Techniky dotazování +=================== + + +Podmínky WHERE +-------------- + +Podmínky WHERE můžete zapsat jako asociativní pole, kde klíče jsou názvy sloupců a hodnoty jsou data pro porovnání. Nette Database automaticky vybere nejvhodnější SQL operátor podle typu hodnoty. + +```php +$database->query('SELECT * FROM users WHERE', [ + 'name' => 'John', + 'active' => true, +]); +// WHERE `name` = 'John' AND `active` = 1 +``` + +V klíči můžete také explicitně specifikovat operátor pro porovnání: + +```php +$database->query('SELECT * FROM users WHERE', [ + 'age >' => 25, // použije operátor > + 'name LIKE' => '%John%', // použije operátor LIKE + 'email NOT LIKE' => '%example.com%', // použije operátor NOT LIKE +]); +// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' +``` + +Nette automaticky ošetřuje speciální případy jako `null` hodnoty nebo pole. + +```php +$database->query('SELECT * FROM products WHERE', [ + 'name' => 'Laptop', // použije operátor = + 'category_id' => [1, 2, 3], // použije IN + 'description' => null, // použije IS NULL +]); +// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL +``` + +Pro negativní podmínky použijte operátor `NOT`: + +```php +$database->query('SELECT * FROM products WHERE', [ + 'name NOT' => 'Laptop', // použije operátor <> + 'category_id NOT' => [1, 2, 3], // použije NOT IN + 'description NOT' => null, // použije IS NOT NULL + 'id' => [], // vynechá se +]); +// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +``` + +Pro spojování podmínek se používá operátor `AND`. To lze změnit pomocí [zástupného symbolu ?or |#Hinty pro sestavování SQL]. + + +Pravidla ORDER BY +----------------- + +Řazení `ORDER BY` se dá zapsat pomocí pole. V klíčích uvedeme sloupce a hodnotou bude boolean určující, zda řadit vzestupně: + +```php +$database->query('SELECT id FROM author ORDER BY', [ + 'id' => true, // vzestupně + 'name' => false, // sestupně +]); +// SELECT id FROM author ORDER BY `id`, `name` DESC +``` + + +Vkládání dat (INSERT) +--------------------- + +Pro vkládání záznamů se používá SQL příkaz `INSERT`. + +```php +$values = [ + 'name' => 'John Doe', + 'email' => 'john@example.com', +]; +$database->query('INSERT INTO users ?', $values); +$userId = $database->getInsertId(); +``` + +Metoda `getInsertId()` vrátí ID naposledy vloženého řádku. U některých databází (např. PostgreSQL) je nutné jako parametr specifikovat název sekvence, ze které se má ID generovat pomocí `$database->getInsertId($sequenceId)`. + +Jako parametry můžeme předávat i [#speciální hodnoty] jako soubory, objekty DateTime nebo výčtové typy. + +Vložení více záznamů najednou: + +```php +$database->query('INSERT INTO users ?', [ + ['name' => 'User 1', 'email' => 'user1@mail.com'], + ['name' => 'User 2', 'email' => 'user2@mail.com'], +]); +``` + +Vícenásobný INSERT je mnohem rychlejší, protože se provede jediný databázový dotaz, namísto mnoha jednotlivých. + +**Bezpečnostní upozornění:** Nikdy nepoužívejte jako `$values` nevalidovaná data. Seznamte se s [možnými riziky |security#Bezpečná práce se sloupci]. + + +Aktualizace dat (UPDATE) +------------------------ + +Pro aktualizacizáznamů se používá SQL příkaz `UPDATE`. + +```php +// Aktualizace jednoho záznamu +$values = [ + 'name' => 'John Smith', +]; +$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); +``` + +Počet ovlivněných řádků vrátí `$result->getRowCount()`. + +Pro UPDATE můžeme využít operátorů `+=` a `-=`: + +```php +$database->query('UPDATE users SET ? WHERE id = ?', [ + 'login_count+=' => 1, // inkrementace login_count +], 1); +``` + +Příklad vložení, nebo úpravy záznamu, pokud již existuje. Použijeme techniku `ON DUPLICATE KEY UPDATE`: + +```php +$values = [ + 'name' => $name, + 'year' => $year, +]; +$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', + $values + ['id' => $id], + $values, +); +// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) +// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 +``` + +Všimněte si, že Nette Database pozná, v jakém kontextu SQL příkazu parametr s polem vkládáme a podle toho z něj sestaví SQL kód. Takže z prvního pole sestavil `(id, name, year) VALUES (123, 'Jim', 1978)`, zatímco druhé převedl do podoby `name = 'Jim', year = 1978`. Podroběji se tomu věnujeme v části [#Hinty pro sestavování SQL]. + + +Mazání dat (DELETE) +------------------- + +Pro mazání záznamů se používá SQL příkaz `DELETE`. Příklad se získáním počtu smazaných řádků: + +```php +$count = $database->query('DELETE FROM users WHERE id = ?', 1) + ->getRowCount(); +``` + + +Hinty pro sestavování SQL +------------------------- + +Hint je speciální zástupný symbol v SQL dotazu, který říká, jak se má hodnota parametru přepsat do SQL výrazu: + +| Hint | Popis | Automaticky se použije +|-----------|-------------------------------------------------|----------------------------- +| `?name` | použije pro vložení názvu tabulky nebo sloupce | - +| `?values` | vygeneruje `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` +| `?set` | vygeneruje přiřazení `key = value, ...` | `SET ?`, `KEY UPDATE ?` +| `?and` | spojí podmínky v poli operátorem `AND` | `WHERE ?`, `HAVING ?` +| `?or` | spojí podmínky v poli operátorem `OR` | - +| `?order` | vygeneruje klauzuli `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` + +Pro dynamické vkládání názvů tabulek a sloupců do dotazu slouží zástupný symbol `?name`. Nette Database se postará o správné ošetření identifikátorů podle konvencí dané databáze (např. uzavření do zpětných uvozovek v MySQL). + +```php +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); +// SELECT `name` FROM `users` WHERE id = 1 (v MySQL) +``` + +**Upozornění:** symbol `?name` používejte pouze pro názvy tabulek a sloupců z validovaných vstupů, jinak se vystavujete [bezpečnostnímu riziku |security#Dynamické identifikátory]. + +Ostatní hinty obvykle není potřeba uvádět, neboť Nette používá při skládání SQL dotazu chytrou autodetekci (viz třetí sloupec tabulky). Ale můžete jej využít například v situaci, kdy chcete spojit podmínky pomocí `OR` namísto `AND`: + +```php +$database->query('SELECT * FROM users WHERE ?or', [ + 'name' => 'John', + 'email' => 'john@example.com', +]); +// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' +``` + + +Speciální hodnoty +----------------- + +Kromě běžných skalárních typů (string, int, bool) můžete jako parametry předávat i speciální hodnoty: + +- soubory: `fopen('image.gif', 'r')` vloží binární obsah souboru +- datum a čas: objekty `DateTime` se převedou na databázový formát +- výčtové typy: instance `enum` se převedou na jejich hodnotu +- SQL literály: vytvořené pomocí `Connection::literal('NOW()')` se vloží přímo do dotazu + +```php +$database->query('INSERT INTO articles ?', [ + 'title' => 'My Article', + 'published_at' => new DateTime, + 'content' => fopen('image.png', 'r'), + 'state' => Status::Draft, +]); +``` + +U databází, které nemají nativní podporu pro datový typ `datetime` (jako SQLite a Oracle), se `DateTime` převádí na hodnotu určenou v [konfiguraci databáze|configuration] položkou `formatDateTime` (výchozí hodnota je `U` - unix timestamp). + + +SQL literály +------------ + +V některých případech potřebujete jako hodnotu uvést přímo SQL kód, který se ale nemá chápat jako řetězec a escapovat. K tomuto slouží objekty třídy `Nette\Database\SqlLiteral`. Vytváří je metoda `Connection::literal()`. + +```php +$result = $database->query('SELECT * FROM users WHERE', [ + 'name' => $name, + 'year >' => $database::literal('YEAR()'), +]); +// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) +``` + +Nebo alternativě: + +```php +$result = $database->query('SELECT * FROM users WHERE', [ + 'name' => $name, + $database::literal('year > YEAR()'), +]); +// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) +``` + +SQL literály mohou obsahovat parametry: + +```php +$result = $database->query('SELECT * FROM users WHERE', [ + 'name' => $name, + $database::literal('year > ? AND year < ?', $min, $max), +]); +// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) +``` + +Díky čemuž můžeme vytvářet zajímavé kombinace: + +```php +$result = $database->query('SELECT * FROM users WHERE', [ + 'name' => $name, + $database::literal('?or', [ + 'active' => true, + 'role' => $role, + ]), +]); +// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') +``` + + +Získání dat +=========== + + +Zkratky pro SELECT dotazy +------------------------- + +Pro zjednodušení načítání dat nabízí `Connection` několik zkratek, které kombinují volání `query()` s následujícím `fetch*()`. Tyto metody přijímají stejné parametry jako `query()`, tedy SQL dotaz a volitelné parametry. Plnohodnotný popis metod `fetch*()` najdete [níže |#fetch]. + +| `fetch($sql, ...$params): ?Row` | Provede dotaz a vrátí první řádek jako objekt `Row` +| `fetchAll($sql, ...$params): array` | Provede dotaz a vrátí všechny řádky jako pole objektů `Row` +| `fetchPairs($sql, ...$params): array` | Provede dotaz a vrátí asocitivní pole, kde první sloupec představuje klíč a druhý hodnotu +| `fetchField($sql, ...$params): mixed` | Provede dotaz a vrátí hodnotu prvního políčka z prvního řádku +| `fetchList($sql, ...$params): ?array` | Provede dotaz a vrací první řádek jako indexované pole + +Příklad: + +```php +// fetchField() - vrátí hodnotu první buňky +$count = $database->query('SELECT COUNT(*) FROM articles') + ->fetchField(); +``` + + +`foreach` - iterace přes řádky +------------------------------ + +Po vykonání dotazu se vrací objekt [ResultSet|api:Nette\Database\ResultSet], který umožňuje procházet výsledky několika způsoby. Nejsnazší způsob, jak vykonat dotaz a získat řádky, je iterováním v cyklu `foreach`. Tento způsob je paměťově nejúspornější, neboť vrací data postupně a neukládá si je do paměti najednou. + +```php +$result = $database->query('SELECT * FROM users'); + +foreach ($result as $row) { + echo $row->id; + echo $row->name; + // ... +} +``` + +.[note] +`ResultSet` lze iterovat pouze jednou. Pokud potřebujete iterovat opakovaně, musíte nejprve načíst data do pole, například pomocí metody `fetchAll()`. + + +fetch(): ?Row .[method] +----------------------- + +Vrací řádek jako objekt `Row`. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. + +```php +$result = $database->query('SELECT * FROM users'); +$row = $result->fetch(); // načte první řádek +if ($row) { + echo $row->name; +} +``` + + +fetchAll(): array .[method] +--------------------------- + +Vrací všechny zbývající řádky z `ResultSetu` jako pole objektů `Row`. + +```php +$result = $database->query('SELECT * FROM users'); +$rows = $result->fetchAll(); // načte všechny řádky +foreach ($rows as $row) { + echo $row->name; +} +``` + + +fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] +--------------------------------------------------------------------------------------- + +Vrátí výsledky jako asociativní pole. První argument určuje název sloupce, který se použije jako klíč v poli, druhý argument určuje název sloupce, který se použije jako hodnota: + +```php +$result = $database->query('SELECT id, name FROM users'); +$names = $result->fetchPairs('id', 'name'); +// [1 => 'John Doe', 2 => 'Jane Doe', ...] +``` + +Pokud uvedeme pouze první parametr, bude hodnotou celý řádek, tedy objekt `Row`: + +```php +$rows = $result->fetchPairs('id'); +// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] +``` + +V případě duplicitních klíčů se použije hodnota z posledního řádku. Při použití `null` jako klíče bude pole indexováno numericky od nuly (pak ke kolizím nedochází): + +```php +$names = $result->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] +``` + + +fetchPairs(Closure $callback): array .[method] +---------------------------------------------- + +Alternativně můžete jako parametr uvést callback, který bude pro každý řádek vracet buď samotnou hodnotu, nebo dvojici klíč-hodnota. + +```php +$result = $database->query('SELECT * FROM users'); +$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); +// ['1 - John', '2 - Jane', ...] + +// Callback může také vracet pole s dvojicí klíč & hodnota: +$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); +// ['John' => 46, 'Jane' => 21, ...] +``` + + +fetchField(): mixed .[method] +----------------------------- + +Vrací hodnotu prvního políčka z aktuálního řádku. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. + +```php +$result = $database->query('SELECT name FROM users'); +$name = $result->fetchField(); // načte jméno z prvního řádku +``` + + +fetchList(): ?array .[method] +----------------------------- + +Vrací řádek jako indexované pole. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. + +```php +$result = $database->query('SELECT name, email FROM users'); +$row = $result->fetchList(); // ['John', 'john@example.com'] +``` + + +getRowCount(): ?int .[method] +----------------------------- + +Vrací počet ovlivněných řádků posledním dotazem `UPDATE` nebo `DELETE`. Pro `SELECT` je to počet vrácených řádků, ale ten nemusí být znám - v takovém případě metoda vrátí `null`. + + +getColumnCount(): ?int .[method] +-------------------------------- + +Vrací počet sloupců v `ResultSetu`. + + +Informace o dotazech +==================== + +Pro ladicí účely můžeme získat informace o posledním provedeném dotazu: + +```php +echo $database->getLastQueryString(); // vypíše SQL dotaz + +$result = $database->query('SELECT * FROM articles'); +echo $result->getQueryString(); // vypíše SQL dotaz +echo $result->getTime(); // vypíše dobu vykonání v sekundách +``` + +Pro zobrazení výsledku jako HTML tabulky lze použít: + +```php +$result = $database->query('SELECT * FROM articles'); +$result->dump(); +``` + +ResultSet nabízí informace o typech sloupců: + +```php +$result = $database->query('SELECT * FROM articles'); +$types = $result->getColumnTypes(); + +foreach ($types as $column => $type) { + echo "$column je typu $type->type"; // např. 'id je typu int' +} +``` + + +Logování dotazů +--------------- + +Můžeme implementovat vlastní logování dotazů. Událost `onQuery` je pole callbacků, které se zavolají po každém provedeném dotazu: + +```php +$database->onQuery[] = function ($database, $result) use ($logger) { + $logger->info('Query: ' . $result->getQueryString()); + $logger->info('Time: ' . $result->getTime()); + + if ($result->getRowCount() > 1000) { + $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); + } +}; +``` diff --git a/database/cs/transactions.texy b/database/cs/transactions.texy new file mode 100644 index 0000000000..e91070c2fb --- /dev/null +++ b/database/cs/transactions.texy @@ -0,0 +1,43 @@ +Transakce +********* + +.[perex] +Transakce zaručují, že se buď provedou všechny operace v rámci transakce, nebo se neprovede žádná. Jsou užitečné pro zajištění konzistence dat při složitějších operacích. + +Nejjednodušší způsob použití transakcí vypadá takto: + +```php +$database->beginTransaction(); +try { + $database->query('DELETE FROM articles WHERE id = ?', $id); + $database->query('INSERT INTO audit_log', [ + 'article_id' => $id, + 'action' => 'delete' + ]); + $database->commit(); +} catch (\Exception $e) { + $database->rollBack(); + throw $e; +} +``` + +Mnohem elegantněji můžete to samé zapsat pomocí metody `transaction()`. Jako parametr přijímá callback, který vykoná v transakci. Pokud callback proběhne bez výjimky, transakce se automaticky potvrdí. Pokud dojde k výjimce, transakce se zruší (rollback) a výjimka se šíří dál. + +```php +$database->transaction(function ($database) use ($id) { + $database->query('DELETE FROM articles WHERE id = ?', $id); + $database->query('INSERT INTO audit_log', [ + 'article_id' => $id, + 'action' => 'delete' + ]); +}); +``` + +Metoda `transaction()` může také vracet hodnoty: + +```php +$count = $database->transaction(function ($database) { + $result = $database->query('UPDATE users SET active = ?', true); + return $result->getRowCount(); // vrátí počet aktualizovaných řádků +}); +``` diff --git a/database/cs/upgrading.texy b/database/cs/upgrading.texy new file mode 100644 index 0000000000..20bb26d94d --- /dev/null +++ b/database/cs/upgrading.texy @@ -0,0 +1,14 @@ +Upgrade +******* + + +Přechod z verze 3.1 na 3.2 +========================== + +Minimální požadovaná verze PHP je 8.1. + +Kód byl pečlivě vyladěn pro PHP 8.1. Byly doplněny všechny nové typehinty u metod a properties. Změny jsou jen drobné: + +- MySQL: nulové datum `0000-00-00` vrací jako `null` +- MySQL: decimal bez desetinných míst vrací jako int místo float +- typ `time` vrací jako DateTime s datumem `1. 1. 0001` místo aktuálního data diff --git a/database/en/@home.texy b/database/en/@home.texy index e770287bac..7b34e23d7a 100644 --- a/database/en/@home.texy +++ b/database/en/@home.texy @@ -1,7 +1,7 @@ -Supported Servers -================= +Supported Databases +=================== These database servers are supported: @@ -17,4 +17,4 @@ These database servers are supported: {{title: Nette Database}} -{{description: Nette Database significantly simplifies retrieving data from the database without writing SQL queries. It uses efficient queries and does not transmit unnecessary data.}} +{{description: Nette Database significantly simplifies retrieving data from the database without writing SQL queries. It executes efficient queries and does not transfer unnecessary data.}} diff --git a/database/en/@left-menu.texy b/database/en/@left-menu.texy index 3a2cf9ca88..f865f8027e 100644 --- a/database/en/@left-menu.texy +++ b/database/en/@left-menu.texy @@ -1,5 +1,12 @@ -Database -******** -- [Core] +Nette Database +************** +- [Getting Started |guide] +- [SQL Way] - [Explorer] +- [Transactions] +- [Exceptions] +- [Reflection] +- [Mapping] - [Configuration] +- [Security Risks |security] +- [Upgrading] diff --git a/database/en/@meta.texy b/database/en/@meta.texy new file mode 100644 index 0000000000..42471908b0 --- /dev/null +++ b/database/en/@meta.texy @@ -0,0 +1 @@ +{{sitename: Nette Documentation}} diff --git a/database/en/configuration.texy b/database/en/configuration.texy index 0e8313c59e..8a11a1cbcb 100644 --- a/database/en/configuration.texy +++ b/database/en/configuration.texy @@ -1,10 +1,10 @@ -Configuring Database -******************** +Database Configuration +********************** .[perex] -Overview of configuration options for the Nette Database. +Overview of configuration options for Nette Database. -If you are not using the whole framework, but only this library, read [how to load the configuration|bootstrap:]. +If you are not using the entire framework, but only this library, read [how to load the configuration|bootstrap:]. Single Connection @@ -14,48 +14,54 @@ Configure a single database connection: ```neon database: - # DSN, only mandatory key + # DSN, the only mandatory key dsn: "sqlite:%appDir%/Model/demo.db" user: ... password: ... ``` -It creates services of type `Nette\Database\Connection` and also `Nette\Database\Explorer` for the [Database Explorer|explorer] layer. The database connection is usually passed by [autowiring |dependency-injection:autowiring], if this is not possible, use the service names `@database.default.connection` resp. `@database.default.explorer`. +This creates the `Nette\Database\Connection` and `Nette\Database\Explorer` services, which are usually passed via [autowiring |dependency-injection:autowiring] or by referring to [their name |#DI Services]. Other settings: ```neon database: - # shows database panel in Tracy Bar? + # show the database panel in Tracy Bar? debugger: ... # (bool) defaults to true - # shows query EXPLAIN in Tracy Bar? + # show query EXPLAIN in Tracy Bar? explain: ... # (bool) defaults to true - # to enable autowiring for this connection? - autowired: ... # (bool) defaults to true for first connection + # enable autowiring for this connection? + autowired: ... # (bool) defaults to true for the first connection # table conventions: discovered, static, or class name conventions: discovered # (string) defaults to 'discovered' options: - # to connect to the database only when needed? + # connect to the database only when needed? lazy: ... # (bool) defaults to false # PHP database driver class driverClass: # (string) - # only MySQL: sets sql_mode + # MySQL only: sets sql_mode sqlmode: # (string) - # only MySQL: sets SET NAMES - charset: # (string) defaults to 'utf8mb4' ('utf8' before v5.5.3) + # MySQL only: sets SET NAMES + charset: # (string) defaults to 'utf8mb4' - # only Oracle and SQLite: date format + # MySQL only: converts TINYINT(1) to bool + convertBoolean: # (bool) defaults to false + + # returns date columns as immutable objects (since version 3.2.1) + newDateTime: # (bool) defaults to false + + # only Oracle and SQLite: format for storing date formatDateTime: # (string) defaults to 'U' ``` -The `options` key can contain other options that can be found in the [PDO driver documentation |https://www.php.net/manual/en/pdo.drivers.php], such as: +The `options` key can contain other options found in the [PDO driver documentation |https://www.php.net/manual/en/pdo.drivers.php], such as: ```neon database: @@ -67,7 +73,7 @@ database: Multiple Connections -------------------- -In the configuration we can define more database connections by dividing them into named sections: +In the configuration, we can define multiple database connections by dividing them into named sections: ```neon database: @@ -80,9 +86,23 @@ database: dsn: 'sqlite::memory:' ``` -Each defined connection creates services that includes section name in their name, ie `@database.main.connection` & `@database.main.explorer` and further `@database.another.connection` & `@database.another.explorer`. +Autowiring is enabled only for services from the first section. This can be changed using `autowired: false` or `autowired: true`. + + +DI Services +----------- + +These services are added to the DI container, where `###` represents the connection name: + +| Name | Type | Description +|---------------------------|---------------------------------|--------------------------- +| `database.###.connection` | [api:Nette\Database\Connection] | database connection +| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] + + +If we define only one connection, the service names will be `database.default.connection` and `database.default.explorer`. If we define multiple connections as in the example above, the names will correspond to the sections, i.e., `database.main.connection`, `database.main.explorer`, and also `database.another.connection` and `database.another.explorer`. -Autowiring is enabled only for services from the first section. This can be changed with `autowired: false` or `autowired: true`. Non-autowired services are passed by name: +We pass non-autowired services explicitly by referencing their name: ```neon services: diff --git a/database/en/core.texy b/database/en/core.texy deleted file mode 100644 index a81fadfb85..0000000000 --- a/database/en/core.texy +++ /dev/null @@ -1,352 +0,0 @@ -Database Core -************* - -.[perex] -Nette Database Core is database abstraction layer and provides core functionality. - - -Installation -============ - -Download and install the package using [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Connection and Configuration -============================ - -To connect to the database, simply create an instance of the [api:Nette\Database\Connection] class: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -The `$dsn` (data source name) parameter is [the same as used by PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], eg `host=127.0.0.1;dbname=test`. In the case of failure it throws `Nette\Database\ConnectionException`. - -However, a more sophisticated way offers [application configuration |configuration]. We will add a `database` section and it creates the required objects and a database panel in the [Tracy |tracy:] bar. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -The connection object we [receive as a service from a DI container |dependency-injection:passing-dependencies], for example: - -```php -class Model -{ - private $database; - - // pass Nette\Database\Explorer to work with the Database Explorer layer - public function __construct(Nette\Database\Connection $database) - { - $this->database = $database; - } -} -``` - -For more information, see [database configuration|configuration]. - - -Queries -======= - -To query database use `query()` method that returns [ResultSet |api:Nette\Database\ResultSet]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} - -echo $result->getRowCount(); // returns the number of rows if is known -``` - -.[note] -Over the `ResultSet` is possible to iterate only once, if we need to iterate multiple times, it is necessary to convert the result to the array via `fetchAll()` method. - -You can easily add parameters to the query, note the question mark: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); - -$database->query('SELECT * FROM users WHERE name = ? AND active = ?', $name, $active); - -$database->query('SELECT * FROM users WHERE id IN (?)', $ids); // $ids is array -``` - -<div class=warning> -WARNING, never concatenate strings to avoid [SQL injection vulnerability |https://en.wikipedia.org/wiki/SQL_injection]! -/-- -$db->query('SELECT * FROM users WHERE name = ' . $name); // WRONG!!! -\-- -</div> - -In the case of failure `query()` throws either `Nette\Database\DriverException` or one of its descendants: - -- [ConstraintViolationException |api:Nette\Database\ConstraintViolationException] - violation of any constraint -- [ForeignKeyConstraintViolationException |api:Nette\Database\ForeignKeyConstraintViolationException] - invalid foreign key -- [NotNullConstraintViolationException |api:Nette\Database\NotNullConstraintViolationException] - violation of the NOT NULL condition -- [UniqueConstraintViolationException |api:Nette\Database\UniqueConstraintViolationException] - conflict of unique index - -In addition to `query()`, there are other useful methods: - -```php -// returns the associative array id => name -$pairs = $database->fetchPairs('SELECT id, name FROM users'); - -// returns all rows as array -$rows = $database->fetchAll('SELECT * FROM users'); - -// returns single row -$row = $database->fetch('SELECT * FROM users WHERE id = ?', $id); - -// return single field -$name = $database->fetchField('SELECT name FROM users WHERE id = ?', $id); -``` - -In case of failure, all of these methods throw `Nette\Database\DriverException.` - - -Insert, Update & Delete -======================= - -The parameter that we insert into the SQL query can also be the array (in which case it is possible to skip the wildcard `?`), which may be useful for the `INSERT` statement: - -```php -$database->query('INSERT INTO users ?', [ // here can be omitted question mark - 'name' => $name, - 'year' => $year, -]); -// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978) - -$id = $database->getInsertId(); // returns the auto-increment of inserted row - -$id = $database->getInsertId($sequence); // or sequence value -``` - -Multiple insert: - -```php -$database->query('INSERT INTO users', [ - [ - 'name' => 'Jim', - 'year' => 1978, - ], [ - 'name' => 'Jack', - 'year' => 1987, - ] -]); -// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978), ('Jack', 1987) -``` - -We can also pass files, DateTime objects or [enumerations |https://www.php.net/enumerations]: - -```php -$database->query('INSERT INTO users', [ - 'name' => $name, - 'created' => new DateTime, // or $database::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // inserts file contents - 'status' => State::New, // enum State -]); -``` - -Updating rows: - -```php -$result = $database->query('UPDATE users SET', [ - 'name' => $name, - 'year' => $year, -], 'WHERE id = ?', $id); -// UPDATE users SET `name` = 'Jim', `year` = 1978 WHERE id = 123 - -echo $result->getRowCount(); // returns the number of affected rows -``` - -For UPDATE, we can use operators `+=` and `-=`: - -```php -$database->query('UPDATE users SET', [ - 'age+=' => 1, // note += -], 'WHERE id = ?', $id); -// UPDATE users SET `age` = `age` + 1 -``` - -Deleting: - -```php -$result = $database->query('DELETE FROM users WHERE id = ?', $id); -echo $result->getRowCount(); // returns the number of affected rows -``` - - -Advanced Queries -================ - -Insert or update, if it already exists: - -```php -$database->query('INSERT INTO users', [ - 'id' => $id, - 'name' => $name, - 'year' => $year, -], 'ON DUPLICATE KEY UPDATE', [ - 'name' => $name, - 'year' => $year, -]); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Note that Nette Database recognizes the SQL context in which the array parameter is inserted and builds the SQL code accordingly. So, from the first array he generates `(id, name, year) VALUES (123, 'Jim', 1978)`, while the second converts to `name = 'Jim', year = 1978`. - -We can also describe sorting using array, in keys are column names and values are boolean that determines whether to sort in ascending order: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // ascending - 'name' => false, // descending -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - -If the detection did not work, you can specify the form of the assembly with a wildcard `?` followed by a hint. These hints are supported: - -| ?values | (key1, key2, ...) VALUES (value1, value2, ...) -| ?set | key1 = value1, key2 = value2, ... -| ?and | key1 = value1 AND key2 = value2 ... -| ?or | key1 = value1 OR key2 = value2 ... -| ?order | key1 ASC, key2 DESC - -The WHERE clause uses the `?and` operator so conditions are linked by `AND`: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year' => $year, -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND `year` = 1978 -``` - -Which can easily be changed to `OR` by using the `?or` wildcard: - -```php -$result = $database->query('SELECT * FROM users WHERE ?or', [ - 'name' => $name, - 'year' => $year, -]); -// SELECT * FROM users WHERE `name` = 'Jim' OR `year` = 1978 -``` - -We can use operators in conditions: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name <>' => $name, - 'year >' => $year, -]); -// SELECT * FROM users WHERE `name` <> 'Jim' AND `year` > 1978 -``` - -And also enumerations: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => ['Jim', 'Jack'], - 'role NOT IN' => ['admin', 'owner'], // enumeration + operator NOT IN -]); -// SELECT * FROM users WHERE -// `name` IN ('Jim', 'Jack') AND `role` NOT IN ('admin', 'owner') -``` - -We can also include a piece of custom SQL code using the so-called SQL literal: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Alternatively: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL literal also can have its parameters: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Thanks to which we can create interesting combinations: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Variable Name -============= - -There is a `?name` wildcard that you use if the table name or column name is a variable. (Beware, do not allow the user to manipulate the content of such a variable): - -```php -$table = 'blog.users'; -$column = 'name'; -$database->query('SELECT * FROM ?name WHERE ?name = ?', $table, $column, $name); -// SELECT * FROM `blog`.`users` WHERE `name` = 'Jim' -``` - - -Transactions -============ - -There are three methods for dealing with transactions: - -```php -$database->beginTransaction(); - -$database->commit(); - -$database->rollback(); -``` - -An elegant way is offered by the `transaction()` method. You pass the callback that is executed in the transaction. If an exception is thrown during execution, the transaction is dropped, if everything goes well, the transaction is committed. - -```php -$id = $database->transaction(function ($database) { - $database->query('DELETE FROM ...'); - $database->query('INSERT INTO ...'); - // ... - return $database->getInsertId(); -}); -``` - -As you can see, the `transaction()` method returns the return value of the callback. - -The transaction() can also be nested, which simplifies the implementation of independent repositories. diff --git a/database/en/exceptions.texy b/database/en/exceptions.texy new file mode 100644 index 0000000000..d3780dcacc --- /dev/null +++ b/database/en/exceptions.texy @@ -0,0 +1,34 @@ +Exceptions +********** + +Nette Database uses an exception hierarchy. The base class is `Nette\Database\DriverException`, which extends `PDOException` and provides enhanced functionality for working with database errors: + +- The `getDriverCode()` method returns the error code from the database driver. +- The `getSqlState()` method returns the SQLSTATE code. +- The `getQueryString()` and `getParameters()` methods allow retrieving the original query and its parameters. + +The `DriverException` class is extended by the following specialized exceptions: + +- `ConnectionException` – indicates a failure to connect to the database server. +- `ConstraintViolationException` – the base class for database constraint violations, from which the following exceptions inherit: + - `ForeignKeyConstraintViolationException` – violation of a foreign key constraint. + - `NotNullConstraintViolationException` – violation of a NOT NULL constraint. + - `UniqueConstraintViolationException` – violation of a uniqueness constraint. + + +The following example demonstrates how to catch a `UniqueConstraintViolationException`, which occurs when trying to insert a user with an email that already exists in the database (assuming the `email` column has a unique index): + +```php +try { + $database->query('INSERT INTO users', [ + 'email' => 'john@example.com', + 'name' => 'John Doe', + 'password' => $hashedPassword, + ]); +} catch (Nette\Database\UniqueConstraintViolationException $e) { + echo 'A user with this email already exists.'; + +} catch (Nette\Database\DriverException $e) { + echo 'An error occurred during registration: ' . $e->getMessage(); +} +``` diff --git a/database/en/explorer.texy b/database/en/explorer.texy index 41c2ed2d2f..dc5e7ef7af 100644 --- a/database/en/explorer.texy +++ b/database/en/explorer.texy @@ -3,548 +3,910 @@ Database Explorer <div class=perex> -Nette Database Explorer significantly simplifies retrieving data from the database without writing SQL queries. +Explorer offers an intuitive and efficient way to work with your database. It automatically handles table relationships and optimizes queries, allowing you to focus on your application logic. It works immediately without configuration. If you need full control over SQL queries, you can use the [SQL way |SQL way]. -- uses efficient queries -- no data is transmitted unnecessarily -- features elegant syntax +- Working with data is natural and easy to understand +- Generates optimized SQL queries that fetch only the necessary data +- Provides easy access to related data without the need to write JOIN queries +- Works immediately without any configuration or entity generation </div> -To use Database Explorer, start with a table - call `table()` on a [api:Nette\Database\Explorer] object. The easiest way to get a context object instance is [described here |core#Connection and Configuration], or, for case when Nette Database Explorer is used as a standalone tool, it can be [created manually|#Creating Explorer Manually]. + +Working with the Explorer begins by calling the `table()` method on the [api:Nette\Database\Explorer] object (see [Connection and Configuration |guide#Connection and Configuration] for details on setting up the database connection): ```php -$books = $explorer->table('book'); // db table name is 'book' +$books = $explorer->table('book'); // 'book' is the table name ``` -The call returns an instance of [Selection |api:Nette\Database\Table\Selection] object, that can be iterated over to retrieve all the books. Each item (a row) is represented by an instance of [ActiveRow |api:Nette\Database\Table\ActiveRow] with data mapped to its properties: +The method returns a [Selection |api:Nette\Database\Table\Selection] object, which represents an SQL query. Additional methods can be chained to this object for filtering and sorting results. The query is assembled and executed only when the data is requested, for example, by iterating with `foreach`. Each row is represented by an [ActiveRow |api:Nette\Database\Table\ActiveRow] object: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // outputs the 'title' column + echo $book->author_id; // outputs the 'author_id' column } ``` -Getting just one specific row is done by `get()` method, which directly returns an ActiveRow instance. +Explorer greatly simplifies working with [table relationships |#Relationships Between Tables]. The following example shows how easily we can output data from related tables (books and their authors). Notice that no JOIN queries need to be written; Nette generates them for us: ```php -$book = $explorer->table('book')->get(2); // returns book with id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // creates a JOIN to the 'author' table +} ``` -Let's take a look at common use-case. You need to fetch books and their authors. It is a common 1:N relationship. The often used solution is to fetch data using one SQL query with table joins. The second possibility is to fetch data separately, run one query for getting books and then get an author for each book by another query (e.g. in your foreach cycle). This could be easily optimized to run only two queries, one for the books, and another for the needed authors - and that is exactly the way how Nette Database Explorer does it. +Nette Database Explorer optimizes queries for maximum efficiency. The above example performs only two SELECT queries, regardless of whether we process 10 or 10,000 books. -In the examples below, we will work with the database schema in the figure. There are OneHasMany (1:N) links (author of book `author_id` and possible translator `translator_id`, which may be `null`) and ManyHasMany (M:N) link between book and its tags. +Additionally, Explorer tracks which columns are used in the code and fetches only those from the database, saving further performance. This behavior is fully automatic and adaptive. If you later modify the code to use additional columns, Explorer automatically adjusts the queries. You don’t need to configure anything or think about which columns will be needed — leave that to Nette. -[An example, including a schema, is found on GitHub |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Database structure used in the examples .<> +Filtering and Sorting +===================== -The following code lists the author's name for each book and all its tags. We will [discuss in a moment |#Working with relationships] how this works internally. +The `Selection` class provides methods for filtering and sorting data selections. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Adds a WHERE condition. Multiple conditions are combined using AND | +| `whereOr(array $conditions)` | Adds a group of WHERE conditions combined using OR | +| `wherePrimary($value)` | Adds a WHERE condition based on the primary key | +| `order($columns, ...$params)` | Sets sorting with ORDER BY | +| `select($columns, ...$params)` | Specifies which columns to fetch | +| `limit($limit, $offset = null)` | Limits the number of rows (LIMIT) and optionally sets OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Sets pagination | +| `group($columns, ...$params)` | Groups rows (GROUP BY) | +| `having($condition, ...$params)`| Adds a HAVING condition for filtering grouped rows | -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author is row from table 'author' +Methods can be chained (the so-called [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. - echo 'tags: '; - foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name . ', '; // $bookTag->tag is row from table 'tag' - } -} -``` +In these methods, you can also use special notations for accessing [data from related tables |#Querying Through Related Tables]. -You will be pleased how efficiently the database layer works. The example above makes a constant number of requests that look like this: -```sql -SELECT * FROM `book` -SELECT * FROM `author` WHERE (`author`.`id` IN (11, 12)) -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 4, 2, 3)) -SELECT * FROM `tag` WHERE (`tag`.`id` IN (21, 22, 23)) -``` +Escaping and Identifiers +------------------------ -If you use [cache |caching:] (defaults on), no columns will be queried unnecessarily. After the first query, cache will store the used column names and Nette Database Explorer will run queries only with the needed columns: +The methods automatically escape parameters and quote identifiers (table and column names), preventing SQL injection. To ensure proper operation, a few rules must be followed: -```sql -SELECT `id`, `title`, `author_id` FROM `book` -SELECT `id`, `name` FROM `author` WHERE (`author`.`id` IN (11, 12)) -SELECT `book_id`, `tag_id` FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 4, 2, 3)) -SELECT `id`, `name` FROM `tag` WHERE (`tag`.`id` IN (21, 22, 23)) +- Write keywords, function names, procedures, etc., in **uppercase**. +- Write column and table names in **lowercase**. +- Always pass strings using **parameters**. + +```php +where('name = ' . $name); // CRITICAL VULNERABILITY: SQL injection +where('name LIKE "%search%"'); // WRONG: complicates automatic quoting +where('name LIKE ?', '%search%'); // CORRECT: value passed as a parameter + +where('name like ?', $name); // WRONG: generates: `name` `like` ? +where('name LIKE ?', $name); // CORRECT: generates: `name` LIKE ? +where('LOWER(name) = ?', $value);// CORRECT: LOWER(`name`) = ? ``` -Selections -========== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -See possibilities how to filter and restrict rows [api:Nette\Database\Table\Selection]: +Filters results using WHERE conditions. Its strength lies in intelligently handling various value types and automatically selecting appropriate SQL operators. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Set WHERE using AND as a glue if two or more conditions are supplied -| `$table->whereOr($where)` | Set WHERE using OR as a glue if two or more conditions are supplied -| `$table->order($columns)` | Set ORDER BY, can be expression `('column DESC, id DESC')` -| `$table->select($columns)` | Set retrieved columns, can be expression `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Set LIMIT and OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Enables pagination -| `$table->group($columns)` | Set GROUP BY -| `$table->having($having)` | Set HAVING +Basic usage: -Fluent interface can be used, for example `$table->where(...)->order(...)->limit(...)`. Multiple `where` or `whereOr` conditions are connected with the `AND` operator. +```php +$table->where('id', $value); // WHERE `id` = 123 +$table->where('id > ?', $value); // WHERE `id` > 123 +$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' +``` +Thanks to automatic detection of suitable operators, you don’t need to handle various special cases — Nette resolves them for you: -where() -------- +```php +$table->where('id', 1); // WHERE `id` = 1 +$table->where('id', null); // WHERE `id` IS NULL +$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) +// You can also use the placeholder ? without an operator: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer can automatically add needed operators for passed values: +The method correctly handles negative conditions and empty arrays: -.[language-php] -| `$table->where('field', $value)` | field = $value -| `$table->where('field', null)` | field IS NULL -| `$table->where('field > ?', $val)` | field > $val -| `$table->where('field', [1, 2])` | field IN (1, 2) -| `$table->where('id = ? OR name = ?', 1, $name)` | id = 1 OR name = 'Jon Snow' -| `$table->where('field', $explorer->table($tableName))` | field IN (SELECT $primary FROM $tableName) -| `$table->where('field', $explorer->table($tableName)->select('col'))` | field IN (SELECT col FROM $tableName) +```php +$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- finds nothing +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- finds everything +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- finds everything +// $table->where('NOT id ?', $ids); // WARNING: This syntax is not supported +``` -You can provide placeholder even without column operator. These calls are the same. +You can also pass the result of another table query as a parameter, creating a subquery: ```php -$table->where('id = ? OR id = ?', 1, 2); -$table->where('id ? OR id ?', 1, 2); +// WHERE `id` IN (SELECT `id` FROM `tableName`) +$table->where('id', $explorer->table($tableName)); + +// WHERE `id` IN (SELECT `col` FROM `tableName`) +$table->where('id', $explorer->table($tableName)->select('col')); ``` -This feature allows to generate correct operator based on value: +Conditions can also be passed as an array, whose items are combined using AND: ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -Selection correctly handles also negative conditions, works for empty arrays too: +In the array, you can use key => value pairs, and Nette will again automatically choose the correct operators: ```php -$table->where('id', []); // id IS NULL AND FALSE -$table->where('id NOT', []); // id IS NULL OR TRUE -$table->where('NOT (id ?)', $ids); // NOT (id IS NULL AND FALSE) +// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) +$table->where([ + 'status' => 'active', + 'id' => [1, 2, 3], +]); +``` -// this will throws an exception, this syntax is not supported -$table->where('NOT id ?', $ids); +In the array, you can combine SQL expressions with placeholders and multiple parameters. This is suitable for complex conditions with precisely defined operators: + +```php +// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // two parameters are passed as an array +]); ``` +Multiple calls to `where()` automatically combine conditions using AND. + -whereOr() ---------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Example of use without parameters: +Similar to `where()`, this adds conditions, but combines them using OR: ```php -// WHERE (user_id IS NULL) OR (SUM(`field1`) > SUM(`field2`)) +// WHERE (`status` = 'active') OR (`deleted` = 1) $table->whereOr([ - 'user_id IS NULL', - 'SUM(field1) > SUM(field2)', + 'status' => 'active', + 'deleted' => true, ]); ``` -We use the parameters. If you do not specify an operator, Nette Database Explorer will automatically add the appropriate one: +More complex expressions can also be used here: ```php -// WHERE (`field1` IS NULL) OR (`field2` IN (3, 5)) OR (`amount` > 11) +// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) $table->whereOr([ - 'field1' => null, - 'field2' => [3, 5], - 'amount >' => 11, + 'price > ?' => 1000, + 'price_with_tax > ?' => 1500, ]); ``` -The key can contain an expression containing wildcard question marks and then pass parameters in the value: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Adds a condition for the table's primary key: ```php -// WHERE (`id` > 12) OR (ROUND(`id`, 5) = 3) -$table->whereOr([ - 'id > ?' => 12, - 'ROUND(id, ?) = ?' => [5, 3], -]); +// WHERE `id` = 123 +$table->wherePrimary(123); + +// WHERE `id` IN (1, 2, 3) +$table->wherePrimary([1, 2, 3]); ``` +If the table has a composite primary key (e.g., `foo_id`, `bar_id`), pass it as an array: + +```php +// WHERE `foo_id` = 1 AND `bar_id` = 5 +$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); + +// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) +$table->wherePrimary([ + ['foo_id' => 1, 'bar_id' => 5], + ['foo_id' => 2, 'bar_id' => 3], +])->fetchAll(); +``` -order() -------- -Examples of use: +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Specifies the order in which rows are returned. You can sort by one or more columns, in ascending or descending order, or according to a custom expression: ```php -$table->order('field1'); // ORDER BY `field1` -$table->order('field1 DESC, field2'); // ORDER BY `field1` DESC, `field2` -$table->order('field = ? DESC', 123); // ORDER BY `field` = 123 DESC +$table->order('created'); // ORDER BY `created` +$table->order('created DESC'); // ORDER BY `created` DESC +$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` +$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC ``` -select() --------- +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- -Examples of use: +Specifies the columns to be returned from the database. By default, Nette Database Explorer returns only the columns that are actually used in the code. Use the `select()` method when you need to retrieve specific expressions: ```php -$table->select('field1'); // SELECT `field1` -$table->select('col, UPPER(col) AS abc'); // SELECT `col`, UPPER(`col`) AS abc -$table->select('SUBSTR(title, ?)', 3); // SELECT SUBSTR(`title`, 3) +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Aliases defined using `AS` are then accessible as properties of the `ActiveRow` object: -limit() -------- +```php +foreach ($table as $row) { + echo $row->formatted_date; // access the alias +} +``` -Examples of use: + +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +Limits the number of returned rows (LIMIT) and optionally allows setting an offset: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (returns the first 10 rows) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +For pagination, it is more appropriate to use the `page()` method. + -page() ------- +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- -An alternative way to set the limit and offset: +Facilitates pagination of results. It accepts the page number (starting from 1) and the number of items per page. Optionally, you can pass a reference to a variable where the total number of pages will be stored: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Getting the last page number, passed to the `$lastPage` variable: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Groups rows according to the specified columns (GROUP BY). It is typically used in conjunction with aggregate functions: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Counts the number of products in each category +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() -------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Examples of use: +Sets a condition for filtering grouped rows (HAVING). It can be used in conjunction with the `group()` method and aggregate functions: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Finds categories that have more than 100 products +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() --------- +Reading Data +============ + +For reading data from the database, several useful methods are available: + +.[language-php] +| `foreach ($table as $key => $row)` | Iterates through all rows, `$key` is the primary key value, `$row` is an ActiveRow object | +| `$row = $table->get($key)` | Returns a single row by primary key | +| `$row = $table->fetch()` | Returns the current row and advances the pointer to the next one | +| `$array = $table->fetchPairs()` | Creates an associative array from the results | +| `$array = $table->fetchAll()` | Returns all rows as an array | +| `count($table)` | Returns the number of rows in the Selection object | + +The [ActiveRow |api:Nette\Database\Table\ActiveRow] object is read-only. This means you cannot change the values of its properties. This restriction ensures data consistency and prevents unexpected side effects. Data is loaded from the database, and any changes should be made explicitly and in a controlled manner. + -Examples of use: +`foreach` - Iterating Through All Rows +-------------------------------------- + +The easiest way to execute a query and retrieve rows is by iterating using a `foreach` loop. It automatically executes the SQL query. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key is the primary key value, $book is ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Filtering by Another Table Value .[#toc-joining-key] ----------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Executes the SQL query and returns the row by primary key, or `null` if it doesn't exist. -Quite often you need filter results by some condition which involves another database table. These types of condition require table join. However, you don't need to write them anymore. +```php +$book = $explorer->table('book')->get(123); // returns ActiveRow with ID 123 or null +if ($book) { + echo $book->title; +} +``` -Let's say you need to get all books whose author's name is 'Jon'. All you need to write is the joining key of the relation and the column name in the joined table. The joining key is derived from the column which refers to the table you want to join. In our example (see the db schema) it is the column `author_id`, and it is sufficient to use just the first part of it - `author` (the `_id` suffix can be omitted). `name` is a column in the `author` table we would like to use. A condition for book translator (which is connected by `translator_id` column) can be created just as easily. + +fetch(): ?ActiveRow .[method] +----------------------------- + +Returns the current row and advances the internal pointer to the next one. If there are no more rows, it returns `null`. ```php $books = $explorer->table('book'); -$books->where('author.name LIKE ?', '%Jon%'); -$books->where('translator.name', 'David Grudl'); +while ($book = $books->fetch()) { + $this->processBook($book); +} ``` -The joining key logic is driven by implementation of [Conventions |api:Nette\Database\Conventions]. We encourage to use [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], which analyzes your foreign keys and allows you to easily work with these relationships. -The relationship between book and its author is 1:N. The reverse relationship is also possible. We call it **backjoin**. Take a look at another example. We would like to fetch all authors, who have written more than 3 books. To make the join reverse we use `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY` statement, also the condition has to be written in form of `HAVING` statement. +fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] +--------------------------------------------------------------------------------------- + +Returns results as an associative array. The first argument specifies the column name to be used as the key in the array, the second argument specifies the column name to be used as the value: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book.id) > 3'); +$authors = $explorer->table('author')->fetchPairs('id', 'name'); +// [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -You may have noticed that the joining expression refers to the book, but it's not clear, whether we are joining through `author_id` or `translator_id`. In the example above, Selection joins through the `author_id` column because a match with the source table has been found - the `author` table. If there was no such a match and there would be more possibilities, Nette would throw [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +If only the first parameter is provided, the value will be the entire row, i.e., the `ActiveRow` object: -To make a join through `translator_id` column, provide an optional parameter within the joining expression. +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + +In case of duplicate keys, the value from the last row is used. When using `null` as the key, the array will be indexed numerically starting from zero (then no collisions occur): ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book(translator).id) > 3'); +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] ``` -Let's take a look at some more difficult joining expression. -We would like to find all authors who have written something about PHP. All books have tags so we should select those authors who have written any book with the PHP tag. +fetchPairs(Closure $callback): array .[method] +---------------------------------------------- + +Alternatively, you can pass a callback as a parameter, which will return either a single value or a key-value pair for each row. ```php -$authors = $explorer->table('author'); -$authors->where(':book:book_tags.tag.name', 'PHP') - ->group('author.id') - ->having('COUNT(:book:book_tags.tag.id) > 0'); +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// ['First Book (John Novak)', ...] + +// The callback can also return an array with a key & value pair: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['First Book' => 'John Novak', ...] ``` -Aggregate Queries ------------------ +fetchAll(): array .[method] +--------------------------- -| `$table->count('*')` | Get number of rows -| `$table->count("DISTINCT $column")` | Get number of distinct values -| `$table->min($column)` | Get minimum value -| `$table->max($column)` | Get maximum value -| `$table->sum($column)` | Get the sum of all values -| `$table->aggregation("GROUP_CONCAT($column)")` | Run any aggregation function +Returns all rows as an associative array of `ActiveRow` objects, where the keys are the primary key values. -.[caution] -The `count()` method without any specified parameters selects all records and returns the array size, which is very inefficient. For example, if you need to calculate the number of rows for paging, always specify the first argument. +```php +$allBooks = $explorer->table('book')->fetchAll(); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + + +count(): int .[method] +---------------------- + +The `count()` method without a parameter returns the number of rows in the `Selection` object: + +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternative +``` +Note: `count()` with a parameter performs the COUNT aggregate function in the database, see below. -Escaping & Quoting -================== -Database Explorer is smart and escape parameters and quotes identificators for you. These basic rules need to be followed, though: +ActiveRow::toArray(): array .[method] +------------------------------------- -- keywords, functions, procedures must be uppercase -- columns and tables must be lowercase -- pass variables as parameters, do not concatenate +Converts the `ActiveRow` object to an associative array, where keys are column names and values are the corresponding data. ```php -->where('name like ?', 'John'); // WRONG! generates: `name` `like` ? -->where('name LIKE ?', 'John'); // CORRECT +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray will be ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + + +Aggregation +=========== + +The `Selection` class provides methods for easily performing aggregate functions (COUNT, SUM, MIN, MAX, AVG, etc.). + +.[language-php] +| `count($expr)` | Counts the number of rows | +| `min($expr)` | Returns the minimum value in a column | +| `max($expr)` | Returns the maximum value in a column | +| `sum($expr)` | Returns the sum of values in a column | +| `aggregation($function)` | Allows any aggregation function, such as `AVG()` or `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +Executes an SQL query with the COUNT function and returns the result. The method is used to determine how many rows match a certain condition: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` +``` + +Note: [#count()] without a parameter only returns the number of rows in the `Selection` object. -->where('KEY = ?', $value); // WRONG! KEY is a keyword -->where('key = ?', $value); // CORRECT. generates: `key` = ? -->where('name = ' . $name); // WRONG! sql injection! -->where('name = ?', $name); // CORRECT +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // WRONG! pass variables as parameters, do not concatenate -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // CORRECT +The `min()` and `max()` methods return the minimum and maximum values in the specified column or expression: + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Returns the sum of values in the specified column or expression: + +```php +// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); ``` -.[warning] -Wrong usage can produce security holes +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Allows performing any aggregate function. + +```php +// average price of products in a category +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// joins product tags into a single string +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +If we need to aggregate results that already result from some aggregate function and grouping (e.g., `SUM(value)` over grouped rows), we specify the aggregate function to be applied to these intermediate results as the second argument: -Fetching Data -============= +```php +// Calculates the total price of products in stock for individual categories and then sums these prices together. +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` -| `foreach ($table as $id => $row)` | Iterate over all rows in result -| `$row = $table->get($id)` | Get single row with ID $id from table -| `$row = $table->fetch()` | Get next row from the result -| `$array = $table->fetchPairs($key, $value)` | Fetch all values to associative array -| `$array = $table->fetchPairs($key)` | Fetch all rows to associative array -| `count($table)` | Get number of rows in result set +In this example, we first calculate the total price of products in each category (`SUM(price * stock) AS category_total`) and group the results by `category_id`. Then, we use `aggregation('SUM(category_total)', 'SUM')` to sum these intermediate totals `category_total`. The second argument `'SUM'` specifies that the SUM function should be applied to the intermediate results. Insert, Update & Delete ======================= -Method `insert()` accepts array of Traversable objects (for example [ArrayHash |utils:arrays#ArrayHash] which returns [forms|forms:]): +Nette Database Explorer simplifies inserting, updating, and deleting data. All the methods mentioned throw a `Nette\Database\DriverException` in case of an error. + + +Selection::insert(iterable $data) .[method] +------------------------------------------- + +Inserts new records into the table. + +**Inserting a single record:** + +Pass the new record as an associative array or iterable object (like `ArrayHash` used in [forms |forms:]), where keys correspond to column names in the table. + +If the table has a defined primary key, the method returns an `ActiveRow` object, which is reloaded from the database to reflect any changes made at the database level (triggers, default column values, auto-increment column calculations). This ensures data consistency, and the object always contains the current data from the database. If it doesn't have a unique primary key, it returns the passed data as an array. ```php $row = $explorer->table('users')->insert([ - 'name' => $name, - 'year' => $year, + 'name' => 'John Doe', + 'email' => 'john.doe@example.com', ]); -// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978) +// $row is an instance of ActiveRow and contains the complete data of the inserted row, +// including the automatically generated ID and any changes made by triggers +echo $row->id; // Outputs the ID of the newly inserted user +echo $row->created_at; // Outputs the creation time if set by a trigger ``` -If primary key is defined on the table, an ActiveRow object containing the inserted row is returned. +**Inserting multiple records at once:** -Multiple insert: +The `insert()` method allows inserting multiple records using a single SQL query. In this case, it returns the number of inserted rows. ```php -$explorer->table('users')->insert([ +$insertedRows = $explorer->table('users')->insert([ + [ + 'name' => 'John', + 'year' => 1994, + ], [ - 'name' => 'Jim', - 'year' => 1978, - ], [ 'name' => 'Jack', - 'year' => 1987, - ] + 'year' => 1995, + ], ]); -// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978), ('Jack', 1987) +// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) +// $insertedRows will be 2 ``` -Files or DateTime objects can be passed as parameters: +A `Selection` object with a data selection can also be passed as a parameter. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); +``` + +**Inserting special values:** + +We can also pass files, `DateTime` objects, or SQL literals as values: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // or $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // inserts the file + 'name' => 'John', + 'created_at' => new DateTime, // converts to database format + 'avatar' => fopen('image.jpg', 'rb'), // inserts binary content of the file + 'uuid' => $explorer::literal('UUID()'), // calls the UUID() function ]); ``` -Updating (returns the count of affected rows): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Updates rows in the table according to the specified filter. Returns the number of actually modified rows. + +Pass the columns to be changed as an associative array or iterable object (like `ArrayHash` used in [forms |forms:]), where keys correspond to column names in the table: ```php -$count = $explorer->table('users') - ->where('id', 10) // must be called before update() +$affected = $explorer->table('users') + ->where('id', 10) ->update([ - 'name' => 'Ned Stark' + 'name' => 'John Smith', + 'year' => 1994, ]); -// UPDATE `users` SET `name`='Ned Stark' WHERE (`id` = 10) +// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 ``` -For update we can use operators `+=` a `-=`: +To change numeric values, you can use the `+=` and `-=` operators: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // increases the value of the 'points' column by 1 + 'coins-=' => 1, // decreases the value of the 'coins' column by 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Deleting (returns the count of deleted rows): + +Selection::delete(): int .[method] +---------------------------------- + +Deletes rows from the table according to the specified filter. Returns the number of deleted rows. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +When calling `update()` or `delete()`, don't forget to use `where()` to specify the rows to be modified/deleted. If `where()` is not used, the operation will be performed on the entire table! + -Working with Relationships -========================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Updates data in the database row represented by the `ActiveRow` object. It accepts an iterable with data to be updated (keys are column names). To change numeric values, you can use the `+=` and `-=` operators: -Has One Relation ----------------- -Has one relation is a common use-case. Book *has one* author. Book *has one* translator. Getting related row is mainly done by `ref()` method. It accepts two arguments: target table name and source joining column. See example: +After performing the update, the `ActiveRow` is automatically reloaded from the database to reflect any changes made at the database level (e.g., triggers). The method returns `true` only if a real data change occurred. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // increments the view count +]); +echo $article->views; // Outputs the current view count ``` -In example above we fetch related author entry from `author` table, the author primary key is searched by `book.author_id` column. Ref() method returns ActiveRow instance or null if there is no appropriate entry. Returned row is an instance of ActiveRow so we can work with it the same way as with the book entry. +This method updates only one specific row in the database. For bulk updates of multiple rows, use the [#Selection::update()] method. + + +ActiveRow::delete(): int .[method] +---------------------------------- + +Deletes the row from the database represented by the `ActiveRow` object. Returns the number of deleted rows, which should be 1. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Deletes the book with ID 1 +``` + +This method deletes only one specific row in the database. For bulk deletion of multiple rows, use the [#Selection::delete()] method. + + +Relationships Between Tables +============================ + +In relational databases, data is divided into multiple tables and linked together using foreign keys. Nette Database Explorer provides a revolutionary way to work with these relationships – without writing JOIN queries and without needing to configure or generate anything. + +To illustrate working with relationships, we'll use an example book database ([find it on GitHub |https://github.com/nette-examples/books]). In the database, we have tables: + +- `author` – writers and translators (columns `id`, `name`, `web`, `born`) +- `book` – books (columns `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` – tags (columns `id`, `name`) +- `book_tag` – junction table between books and tags (columns `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Database structure used in examples .<> + +In our example book database, we find several types of relationships (although the model is simplified compared to reality): + +- **One-to-many (1:N)** – Each book **has one** author; an author can write **multiple** books. +- **Zero-to-many (0:N)** – A book **can have** a translator; a translator can translate **multiple** books. +- **Zero-to-one (0:1)** – A book **can have** a sequel. +- **Many-to-many (M:N)** – A book **can have several** tags, and a tag can be assigned to **several** books. + +In these relationships, there is always a **parent table** and a **child table**. For example, in the relationship between authors and books, the `author` table is the parent, and the `book` table is the child – you can think of it as a book always "belonging" to an author. This is also reflected in the database structure: the child table `book` contains the foreign key `author_id`, which references the parent table `author`. + +If we need to list books including their authors' names, we have two options. Either retrieve the data with a single SQL query using JOIN: -// or directly -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -Book also has one translator, so getting translator name is quite easy. +Or retrieve the data in two steps – first the books, then their authors – and then assemble them in PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors from the selected books +``` + +The second approach is actually **more efficient**, although it might be surprising. Data is fetched only once and can be better utilized in the cache. This is precisely how Nette Database Explorer works – it handles everything under the hood and offers you an elegant API: + ```php -$book->ref('author', 'translator_id')->name +$books = $explorer->table('book'); +foreach ($books as $book) { + echo 'title: ' . $book->title; + echo 'written by: ' . $book->author->name; // $book->author is a record from the 'author' table + echo 'translated by: ' . $book->translator?->name; +} ``` -All of this is fine, but it's somewhat cumbersome, don't you think? Database Explorer already contains the foreign keys definitions so why not use them automatically? Let's do that! -If we call property, which does not exist, ActiveRow tries to resolve the calling property name as 'has one' relation. Getting this property is the same as calling ref() method with just one argument. We will call the only argument the **key**. Key will be resolved to particular foreign key relation. The passed key is matched against row columns, and if it matches, foreign key defined on the matched column is used for getting data from related target table. See example: +Accessing the Parent Table +-------------------------- + +Accessing the parent table is straightforward. These are relationships like *a book has an author* or *a book may have a translator*. The related record is obtained via a property of the ActiveRow object – its name corresponds to the name of the foreign key column without the `_id` suffix: ```php -$book->author->name; -// same as -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // finds the author based on the author_id column +echo $book->translator?->name; // finds the translator based on the translator_id column ``` -The ActiveRow instance has no author column. All book columns are searched for a match with *key*. Matching in this case means the column name has to contain the key. So in the example above, the `author_id` column contains string 'author' and is therefore matched by 'author' key. If you want to get the book translator, just can use e.g. 'translator' as a key, because 'translator' key will match the `translator_id` column. You can find more about the key matching logic in [Joining expressions |#joining-key] chapter. +When accessing the `$book->author` property, Explorer looks in the `book` table for a column whose name contains the string `author` (i.e., `author_id`). Based on the value in this column, it loads the corresponding record from the `author` table and returns it as an `ActiveRow`. Similarly, `$book->translator` uses the `translator_id` column. Since the `translator_id` column can contain `null`, we use the nullsafe operator `?->` in the code. + +An alternative approach is offered by the `ref()` method, which accepts two arguments: the name of the target table and the name of the joining column, and returns an `ActiveRow` instance or `null`: ```php -echo $book->title . ': '; -echo $book->author->name; -if ($book->translator) { - echo ' (translated by ' . $book->translator->name . ')'; -} +echo $book->ref('author', 'author_id')->name; // relationship to the author +echo $book->ref('author', 'translator_id')->name; // relationship to the translator ``` -If you want to fetch multiple books, you should use the same approach. Nette Database Explorer will fetch authors and translators for all the fetched books at once. +The `ref()` method is useful if property-based access cannot be used, for example, because the table contains a column with the same name (i.e., `author`). In other cases, using property-based access is recommended for better readability. + +Explorer automatically optimizes database queries. When iterating through books in a loop and accessing their related records (authors, translators), Explorer does not generate a query for each book separately. Instead, it performs only **one SELECT query for each type of relationship**, significantly reducing the database load. For example: ```php $books = $explorer->table('book'); foreach ($books as $book) { echo $book->title . ': '; echo $book->author->name; - if ($book->translator) { - echo ' (translated by ' . $book->translator->name . ')'; - } + echo $book->translator?->name; } ``` -The code will run only these 3 queries: +This code executes only these three lightning-fast queries to the database: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from the author_id column of selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from the translator_id column of selected books ``` +.[note] +The logic for finding the joining column is determined by the implementation of [Conventions |api:Nette\Database\Conventions]. We recommend using [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], which analyzes foreign keys and allows you to easily work with existing relationships between tables. -Has Many Relation ------------------ -'Has many' relation is just reversed 'has one' relation. Author *has* written *many* books. Author *has* translated *many* books. As you can see, this type of relation is a little bit more difficult because the relation is 'named' ('written', 'translated'). ActiveRow instance has `related()` method, which will return array of related entries. Entries are also ActiveRow instances. See example bellow: +Accessing the Child Table +------------------------- + +Accessing the child table works in the opposite direction. Now we ask *which books did this author write* or *which books did this translator translate*. For this type of query, we use the `related()` method, which returns a `Selection` with related records. Let's look at an example: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Outputs all books by the author foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Outputs all books translated by the author foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Method `related()` method accepts full join description passed as two arguments or as one argument joined by dot. The first argument is the target table, the second is the target column. +The `related()` method accepts the join description as a single argument with dot notation or as two separate arguments: ```php -$author->related('book.translator_id'); -// same as -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // single argument +$author->related('book', 'translator_id'); // two arguments ``` -You can use Nette Database Explorer heuristics based on foreign keys and provide only **key** argument. Key will be matched against all foreign keys pointing into the current table (`author` table). If there is a match, Nette Database Explorer will use this foreign key, otherwise it will throw [Nette\InvalidArgumentException|api:Nette\InvalidArgumentException] or [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. You can find more about the key matching logic in [Joining expressions |#joining-key] chapter. +Explorer can automatically detect the correct joining column based on the name of the parent table. In this case, it joins via the `book.author_id` column because the name of the source table is `author`: + +```php +$author->related('book'); // uses book.author_id +``` -Of course you can call related methods for all fetched authors, Nette Database Explorer will again fetch the appropriate books at once. +If multiple possible connections exist, Explorer will throw an [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +We can, of course, use the `related()` method when iterating through multiple records in a loop, and Explorer will automatically optimize the queries in this case as well: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -Example above will run only two queries: +This code generates only two lightning-fast SQL queries: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Creating Explorer Manually -========================== +Many-to-Many Relationship +------------------------- -A database connection can be created using the application configuration. In such cases a `Nette\Database\Explorer` service is created and can be passed as a dependency using the DI container. +For a many-to-many (M:N) relationship, a **junction table** (in our case, `book_tag`) is needed, containing two foreign key columns (`book_id`, `tag_id`). Each of these columns refers to the primary key of one of the linked tables. To retrieve related data, we first get the records from the junction table using `related('book_tag')` and then proceed to the target data: -However, if Nette Database Explorer is used as a standalone tool, an instance of `Nette\Database\Explorer` object needs to be created manually. +```php +$book = $explorer->table('book')->get(1); +// outputs the names of tags assigned to the book +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // outputs the tag name via the junction table +} + +$tag = $explorer->table('tag')->get(1); +// or the opposite way: outputs the names of books marked with this tag +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // outputs the book title +} +``` + +Explorer again optimizes the SQL queries into an efficient form: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + + +Querying Through Related Tables +------------------------------- + +In the `where()`, `select()`, `order()`, and `group()` methods, you can use special notations to access columns from other tables. Explorer automatically creates the necessary JOINs. + +**Dot notation** (`parent_table.column`) is used for 1:N relationships from the perspective of the child table: ```php -// $storage implements Nette\Caching\Storage: -$storage = new Nette\Caching\Storages\FileStorage($tempDir); -$connection = new Nette\Database\Connection($dsn, $user, $password); -$structure = new Nette\Database\Structure($connection, $storage); -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); +$books = $explorer->table('book'); + +// Finds books whose author's name starts with 'Jon' +$books->where('author.name LIKE ?', 'Jon%'); + +// Sorts books by author name descending +$books->order('author.name DESC'); + +// Outputs the book title and author name +$books->select('book.title, author.name'); ``` + +**Colon notation** (`:child_table.column`) is used for 1:N relationships from the perspective of the parent table: + +```php +$authors = $explorer->table('author'); + +// Finds authors who wrote a book with 'PHP' in the title +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Counts the number of books for each author +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +In the example above with colon notation (`:book.title`), the foreign key column is not specified. Explorer automatically detects the correct column based on the name of the parent table. In this case, it joins via the `book.author_id` column because the name of the source table is `author`. If multiple possible connections exist, Explorer will throw an [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +The joining column can be explicitly specified in parentheses: + +```php +// Finds authors who translated a book with 'PHP' in the title +$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); +``` + +Notations can be chained to access data across multiple tables: + +```php +// Finds authors of books tagged with 'PHP' +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Extending Conditions for JOIN +----------------------------- + +The `joinWhere()` method extends the conditions specified when joining tables in SQL after the `ON` keyword. + +Let's say we want to find books translated by a specific translator: + +```php +// Finds books translated by a translator named 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +In the `joinWhere()` condition, you can use the same constructs as in the `where()` method – operators, placeholders, arrays of values, or SQL expressions. + +For more complex queries with multiple JOINs, you can define table aliases: + +```php +$tags = $explorer->table('tag') + ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) + ->alias(':book_tag.book.author', 'book_author'); +// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` +// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` +// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` +// AND (`book_author`.`born` < 1950) +``` + +Note that while the `where()` method adds conditions to the `WHERE` clause, the `joinWhere()` method extends the conditions in the `ON` clause when joining tables. diff --git a/database/en/guide.texy b/database/en/guide.texy new file mode 100644 index 0000000000..545eb1b656 --- /dev/null +++ b/database/en/guide.texy @@ -0,0 +1,216 @@ +Nette Database +************** + +.[perex] +Nette Database is a powerful and elegant database layer for PHP with a focus on simplicity and smart features. It offers two ways to work with your database: the [Explorer |explorer] for rapid application development, or the [SQL way |SQL way] for direct query manipulation. + +<div class="grid gap-3"> +<div> + + +[SQL way] +========= +- Safe, parameterized queries +- Precise control over SQL query structure +- When writing complex queries with advanced functions +- Optimize performance using specific SQL functions + +</div> + +<div> + + +[Explorer |explorer] +==================== +- Develop quickly without writing SQL +- Intuitive handling of relationships between tables +- Benefit from automatic query optimization +- Suitable for fast and convenient database work + +</div> + +</div> + + +Installation +============ + +Download and install the library using [Composer|best-practices:composer]: + +```shell +composer require nette/database +``` + + +Supported Databases +=================== + +Nette Database supports the following databases: + +|* Database Server |* DSN Name |* Explorer Support +|-----------------------|--------------|-----------------------| +| MySQL (>= 5.1) | mysql | YES | +| PostgreSQL (>= 9.0) | pgsql | YES | +| SQLite 3 (>= 3.8) | sqlite | YES | +| Oracle | oci | NO | +| MS SQL (PDO_SQLSRV) | sqlsrv | YES | +| MS SQL (PDO_DBLIB) | mssql | NO | +| ODBC | odbc | NO | + + +Two Approaches to Database Work +=============================== + +Nette Database gives you a choice: you can either write SQL queries directly (SQL way), or let them be generated automatically (Explorer). Let's see how both approaches handle the same tasks: + +[SQL way|sql-way] - SQL Queries + +```php +// Insert a record +$database->query('INSERT INTO books', [ + 'author_id' => $authorId, + 'title' => $bookData->title, + 'published_at' => new DateTime, +]); + +// Retrieve records: book authors +$result = $database->query(' + SELECT authors.*, COUNT(books.id) AS books_count + FROM authors + LEFT JOIN books ON authors.id = books.author_id + WHERE authors.active = 1 + GROUP BY authors.id +'); + +// Display (not optimal, generates N additional queries) +foreach ($result as $author) { + $books = $database->query(' + SELECT * FROM books + WHERE author_id = ? + ORDER BY published_at DESC + ', $author->id); + + echo "Author $author->name has written $author->books_count books:\n"; + + foreach ($books as $book) { + echo "- $book->title\n"; + } +} +``` + +[Explorer way|explorer] - Automatic SQL Generation + +```php +// Insert a record +$database->table('books')->insert([ + 'author_id' => $authorId, + 'title' => $bookData->title, + 'published_at' => new DateTime, +]); + +// Retrieve records: book authors +$authors = $database->table('authors') + ->where('active', 1); + +// Display (automatically generates only 2 optimized queries) +foreach ($authors as $author) { + $books = $author->related('books') + ->order('published_at DESC'); + + echo "Author $author->name has written {$books->count()} books:\n"; + + foreach ($books as $book) { + echo "- $book->title\n"; + } +} +``` + +The Explorer approach generates and optimizes SQL queries automatically. In the example above, the SQL way generates N+1 queries (one for authors and then one for the books of each author), while Explorer automatically optimizes queries and executes only two — one for authors and one for all their books. + +Both approaches can be freely combined in your application as needed. + + +Connection and Configuration +============================ + +To connect to the database, simply create an instance of the [api:Nette\Database\Connection] class: + +```php +$database = new Nette\Database\Connection($dsn, $user, $password); +``` + +The `$dsn` (Data Source Name) parameter is the same as [used by PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], e.g., `host=127.0.0.1;dbname=test`. In case of failure, it throws a `Nette\Database\ConnectionException`. + +However, a more convenient method is offered by [application configuration |configuration], where you just need to add a `database` section. This creates the necessary objects and also a database panel in the [Tracy |tracy:] bar. + +```neon +database: + dsn: 'mysql:host=127.0.0.1;dbname=test' + user: root + password: password +``` + +Then, the connection object can be [obtained as a service from the DI container |dependency-injection:passing-dependencies], e.g.: + +```php +class Model +{ + public function __construct( + // or Nette\Database\Explorer + private Nette\Database\Connection $database, + ) { + } +} +``` + +More information about [database configuration|configuration]. + + +Manual Creation of Explorer +--------------------------- + +If you are not using the Nette DI container, you can create an instance of `Nette\Database\Explorer` manually: + +```php +// database connection +$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// cache storage, implements Nette\Caching\Storage, e.g.: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// handles database structure reflection +$structure = new Nette\Database\Structure($connection, $storage); +// defines rules for mapping table names, columns, and foreign keys +$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); +$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); +``` + + +Connection Management +===================== + +When a `Connection` object is created, the connection is automatically established. If you want to delay the connection, use lazy mode - enable it in the [configuration|configuration] by setting `lazy`, or like this: + +```php +$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); +``` + +To manage the connection, use the `connect()`, `disconnect()`, and `reconnect()` methods. +- `connect()` creates a connection if one does not already exist, and may throw a `Nette\Database\ConnectionException`. +- `disconnect()` disconnects the current database connection. +- `reconnect()` performs a disconnection and subsequent reconnection to the database. This method may also throw a `Nette\Database\ConnectionException`. + +Additionally, you can monitor events related to the connection using the `onConnect` event, which is an array of callbacks called after the database connection is established. + +```php +// runs after connecting to the database +$database->onConnect[] = function($database) { + echo "Connected to the database"; +}; +``` + + +Tracy Debug Bar +=============== + +If you use [Tracy |tracy:], the Database panel in the Debug Bar is automatically activated. It displays all executed queries, their parameters, execution time, and the location in the code where they were called. + +[* db-panel.webp *] diff --git a/database/en/mapping.texy b/database/en/mapping.texy new file mode 100644 index 0000000000..3a4162363d --- /dev/null +++ b/database/en/mapping.texy @@ -0,0 +1,55 @@ +Type Conversion +*************** + +.[perex] +Nette Database automatically converts values returned from the database to the corresponding PHP types. + + +Date and Time +------------- + +Time values are converted to `Nette\Utils\DateTime` objects. If you want time values to be converted to immutable `Nette\Database\DateTime` objects, set the `newDateTime` option to true in the [configuration |configuration]. + +```php +$row = $database->fetch('SELECT created_at FROM articles'); +echo $row->created_at instanceof DateTime; // true +echo $row->created_at->format('j. n. Y'); +``` + +In the case of MySQL, the `TIME` data type is converted to `DateInterval` objects. + + +Boolean Values +-------------- + +Boolean values are automatically converted to `true` or `false`. For MySQL, `TINYINT(1)` is converted if we set `convertBoolean` in the [configuration |configuration]. + +```php +$row = $database->fetch('SELECT is_published FROM articles'); +echo gettype($row->is_published); // 'boolean' +``` + + +Numeric Values +-------------- + +Numeric values are converted to `int` or `float` according to the column type in the database: + +```php +$row = $database->fetch('SELECT id, price FROM products'); +echo gettype($row->id); // integer +echo gettype($row->price); // float +``` + + +Custom Normalization +-------------------- + +Using the `setRowNormalizer(?callable $normalizer)` method, you can set a custom function for transforming rows from the database. This is useful, for example, for automatic data type conversion. + +```php +$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { + // type conversion happens here + return $row; +}); +``` diff --git a/database/en/reflection.texy b/database/en/reflection.texy new file mode 100644 index 0000000000..c8fdafd987 --- /dev/null +++ b/database/en/reflection.texy @@ -0,0 +1,125 @@ +Structure Reflection +******************** + +.{data-version:3.2.1} +Nette Database provides tools for database structure introspection using the [api:Nette\Database\Reflection] class. It allows obtaining information about tables, columns, indexes, and foreign keys. You can use reflection to generate schemas, create flexible applications working with the database, or general database tools. + +The reflection object is obtained from the database connection instance: + +```php +$reflection = $database->getReflection(); +``` + + +Retrieving Tables +----------------- + +The readonly property `$reflection->tables` contains an associative array of all tables in the database: + +```php +// Listing the names of all tables +foreach ($reflection->tables as $name => $table) { + echo $name . "\n"; +} +``` + +Two additional methods are available: + +```php +// Checking the existence of a table +if ($reflection->hasTable('users')) { + echo "Table users exists"; +} + +// Returns the table object; throws an exception if it does not exist +$table = $reflection->getTable('users'); +``` + + +Table Information +----------------- + +A table is represented by the [Table|api:Nette\Database\Reflection\Table] object, which provides the following readonly properties: + +- `$name: string` – name of the table +- `$view: bool` – whether it is a view +- `$fullName: ?string` – full name of the table including schema (if exists) +- `$columns: array<string, Column>` – associative array of table columns +- `$indexes: Index[]` – array of table indexes +- `$primaryKey: ?Index` – primary key of the table or null +- `$foreignKeys: ForeignKey[]` – array of table foreign keys + + +Columns +------- + +The `columns` property of the table provides an associative array of columns, where the key is the column name and the value is an instance of [Column|api:Nette\Database\Reflection\Column] with these properties: + +- `$name: string` – name of the column +- `$table: ?Table` – reference to the column's table +- `$nativeType: string` – native database type +- `$size: ?int` – size/length of the type +- `$nullable: bool` – whether the column can contain NULL +- `$default: mixed` – default value of the column +- `$autoIncrement: bool` – whether the column is auto-increment +- `$primary: bool` – whether it is part of the primary key +- `$vendor: array` – additional metadata specific to the given database system + +```php +foreach ($table->columns as $name => $column) { + echo "Column: $name\n"; + echo "Type: {$column->nativeType}\n"; + echo "Nullable: " . ($column->nullable ? 'Yes' : 'No') . "\n"; +} +``` + + +Indexes +------- + +The `indexes` property of the table provides an array of indexes, where each index is an instance of [Index|api:Nette\Database\Reflection\Index] with these properties: + +- `$columns: Column[]` – array of columns forming the index +- `$unique: bool` – whether the index is unique +- `$primary: bool` – whether it is a primary key +- `$name: ?string` – name of the index + +The primary key of the table can be obtained using the `primaryKey` property, which returns either an `Index` object or `null` if the table does not have a primary key. + +```php +// Listing indexes +foreach ($table->indexes as $index) { + $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); + echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; + echo " Columns: $columns\n"; + echo " Unique: " . ($index->unique ? 'Yes' : 'No') . "\n"; +} + +// Listing the primary key +if ($primaryKey = $table->primaryKey) { + $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); + echo "Primary Key: $columns\n"; +} +``` + + +Foreign Keys +------------ + +The `foreignKeys` property of the table provides an array of foreign keys, where each foreign key is an instance of [ForeignKey|api:Nette\Database\Reflection\ForeignKey] with these properties: + +- `$foreignTable: Table` – the referenced table +- `$localColumns: Column[]` – array of local columns +- `$foreignColumns: Column[]` – array of referenced columns +- `$name: ?string` – name of the foreign key + +```php +// Listing foreign keys +foreach ($table->foreignKeys as $fk) { + $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); + $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); + + echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; + echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; +} +``` diff --git a/database/en/security.texy b/database/en/security.texy new file mode 100644 index 0000000000..2632e630a1 --- /dev/null +++ b/database/en/security.texy @@ -0,0 +1,185 @@ +Security Risks +************** + +<div class=perex> + +Databases often contain sensitive data and allow performing dangerous operations. For secure work with Nette Database, it is crucial to: + +- Understand the difference between secure and insecure APIs +- Use parameterized queries +- Properly validate input data + +</div> + + +What is SQL Injection? +====================== + +SQL injection is the most serious security risk when working with databases. It occurs when unsanitized user input becomes part of an SQL query. An attacker can insert their own SQL commands and thereby: +- Gain unauthorized access to data +- Modify or delete data in the database +- Bypass authentication + +```php +// ❌ DANGEROUS CODE - vulnerable to SQL injection +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// An attacker might enter a value like: ' OR '1'='1 +// The resulting query would be: SELECT * FROM users WHERE name = '' OR '1'='1' +// Which returns all users +``` + +The same applies to Database Explorer: + +```php +// ❌ DANGEROUS CODE - vulnerable to SQL injection +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Parameterized Queries +===================== + +The fundamental defense against SQL injection is parameterized queries. Nette Database offers several ways to use them. + +The simplest way is to use **question mark placeholders**: + +```php +// ✅ Secure parameterized query +$database->query('SELECT * FROM users WHERE name = ?', $name); + +// ✅ Secure condition in Explorer +$table->where('name = ?', $name); +``` + +This applies to all other methods in [Database Explorer|explorer] that allow inserting expressions with question mark placeholders and parameters. + +For INSERT, UPDATE commands, or the WHERE clause, we can pass values in an array: + +```php +// ✅ Secure INSERT +$database->query('INSERT INTO users', [ + 'name' => $name, + 'email' => $email, +]); + +// ✅ Secure INSERT in Explorer +$table->insert([ + 'name' => $name, + 'email' => $email, +]); +``` + + +Parameter Value Validation +========================== + +Parameterized queries are the cornerstone of secure database work. However, the values we insert into them must undergo several levels of checks: + + +Type Checking +------------- + +**The most important thing is to ensure the correct data type of parameters** – this is a necessary condition for the safe use of Nette Database. The database assumes that all input data has the correct data type corresponding to the given column. + +For example, if `$name` in the previous examples were unexpectedly an array instead of a string, Nette Database would try to insert all its elements into the SQL query, leading to an error. Therefore, **never use** unvalidated data from `$_GET`, `$_POST`, or `$_COOKIE` directly in database queries. + + +Format Validation +----------------- + +At the second level, we check the format of the data – for example, whether strings are in UTF-8 encoding and their length corresponds to the column definition, or whether numerical values are within the allowed range for the given column data type. + +For this level of validation, we can partially rely on the database itself – many databases will reject invalid data. However, behavior can vary; some might silently truncate long strings or clip numbers outside the range. + + +Domain-Specific Validation +-------------------------- + +The third level involves logical checks specific to your application. For example, verifying that values from select boxes match the offered options, that numbers are within the expected range (e.g., age 0-150 years), or that mutual dependencies between values make sense. + + +Recommended Validation Methods +------------------------------ + +- Use [Nette Forms|forms:], which automatically ensure proper validation of all inputs. +- Use [Presenters|application:] and specify data types for parameters in `action*()` and `render*()` methods. +- Or implement your own validation layer using standard PHP tools like `filter_var()`. + + +Safe Work with Columns +====================== + +In the previous section, we showed how to properly validate parameter values. However, when using arrays in SQL queries, we must pay equal attention to their keys. + +```php +// ❌ DANGEROUS CODE - keys in the array are not sanitized +$database->query('INSERT INTO users', $_POST); +``` + +For INSERT and UPDATE commands, this is a critical security flaw – an attacker can insert or modify any column in the database. They could, for example, set `is_admin = 1` or insert arbitrary data into sensitive columns (the so-called Mass Assignment Vulnerability). + +In WHERE conditions, it is even more dangerous because they can contain operators: + +```php +// ❌ DANGEROUS CODE - keys in the array are not sanitized +$_POST['salary >'] = 100000; +$database->query('SELECT * FROM users WHERE', $_POST); +// executes the query WHERE (`salary` > 100000) +``` + +An attacker can use this approach to systematically discover employee salaries. They might start, for example, with a query for salaries above 100,000, then below 50,000, and by gradually narrowing the range, they can reveal the approximate salaries of all employees. This type of attack is called SQL enumeration. + +The `where()` and `whereOr()` methods are even [much more flexible |explorer#where] and support SQL expressions, including operators and functions, in keys and values. This gives an attacker the ability to perform SQL injection: + +```php +// ❌ DANGEROUS CODE - attacker can insert their own SQL +$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; +$table->where($_POST); +// executes the query WHERE (0) UNION SELECT name, salary FROM users WHERE (1) +``` + +This attack terminates the original condition using `0)`, appends its own `SELECT` using `UNION` to obtain sensitive data from the `users` table, and closes the syntactically correct query using `WHERE (1)`. + + +Column Whitelist +---------------- + +For safe work with column names, we need a mechanism that ensures the user can only work with allowed columns and cannot add their own. We could try to detect and block dangerous column names (blacklist), but this approach is unreliable – an attacker can always come up with a new way to write a dangerous column name that we didn't anticipate. + +Therefore, it is much safer to reverse the logic and define an explicit list of allowed columns (whitelist): + +```php +// Columns the user is allowed to modify +$allowedColumns = ['name', 'email', 'active']; + +// Remove all unauthorized columns from the input +$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); + +// ✅ Now safe to use in queries, such as: +$database->query('INSERT INTO users', $filteredData); +$table->update($filteredData); +$table->where($filteredData); +``` + + +Dynamic Identifiers +=================== + +For dynamic table and column names, use the `?name` placeholder. This ensures proper escaping of identifiers according to the syntax of the given database (e.g., using backticks in MySQL): + +```php +// ✅ Safe use of trusted identifiers +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); +// Result in MySQL: SELECT `name` FROM `users` +``` + +Important: Use the `?name` symbol only for trusted values defined in the application code. For values from the user, use a [whitelist |#Column Whitelist] again. Otherwise, you expose yourself to security risks: + +```php +// ❌ DANGEROUS - never use user input +$database->query('SELECT ?name FROM users', $_GET['column']); +``` diff --git a/database/en/sql-way.texy b/database/en/sql-way.texy new file mode 100644 index 0000000000..f447354e3b --- /dev/null +++ b/database/en/sql-way.texy @@ -0,0 +1,513 @@ +SQL Way +******* + +.[perex] +Nette Database offers two ways of working: you can write SQL queries yourself (SQL way), or have them generated automatically (see [Explorer |explorer]). The SQL way gives you full control over the queries while ensuring they are constructed securely. + +.[note] +Details on database connection and configuration can be found in the [Connection and Configuration |guide#Connection and Configuration] chapter. + + +Basic Querying +============== + +The `query()` method is used for database querying. It returns a [ResultSet |api:Nette\Database\ResultSet] object, which represents the query result. If the query fails, the method [throws an exception|exceptions]. You can iterate through the query result using a `foreach` loop, or use one of the [helper methods |#Fetching Data]. + +```php +$result = $database->query('SELECT * FROM users'); + +foreach ($result as $row) { + echo $row->id; + echo $row->name; +} +``` + +To securely insert values into SQL queries, use parameterized queries. Nette Database makes this extremely simple: just add a comma and the value after the SQL query: + +```php +$database->query('SELECT * FROM users WHERE name = ?', $name); +``` + +With multiple parameters, you have two options: You can either interleave the SQL query with parameters: + +```php +$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); +``` + +Or write the entire SQL query first and then append all the parameters: + +```php +$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); +``` + + +Protection Against SQL Injection +================================ + +Why is it important to use parameterized queries? Because they protect you from an attack called SQL injection, where an attacker could inject their own SQL commands and thereby gain access to or damage data in the database. + +.[warning] +**Never insert variables directly into an SQL query!** Always use parameterized queries, which protect you from SQL injection. + +```php +// ❌ DANGEROUS CODE - vulnerable to SQL injection +$database->query("SELECT * FROM users WHERE name = '$name'"); + +// ✅ Secure parameterized query +$database->query('SELECT * FROM users WHERE name = ?', $name); +``` + +Familiarize yourself with [potential security risks |security]. + + +Querying Techniques +=================== + + +WHERE Conditions +---------------- + +You can write `WHERE` conditions as an associative array, where keys are column names and values are the data for comparison. Nette Database automatically selects the most suitable SQL operator based on the value type. + +```php +$database->query('SELECT * FROM users WHERE', [ + 'name' => 'John', + 'active' => true, +]); +// WHERE `name` = 'John' AND `active` = 1 +``` + +You can also explicitly specify the comparison operator in the key: + +```php +$database->query('SELECT * FROM users WHERE', [ + 'age >' => 25, // uses the > operator + 'name LIKE' => '%John%', // uses the LIKE operator + 'email NOT LIKE' => '%example.com%', // uses the NOT LIKE operator +]); +// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' +``` + +Nette automatically handles special cases like `null` values or arrays. + +```php +$database->query('SELECT * FROM products WHERE', [ + 'name' => 'Laptop', // uses the = operator + 'category_id' => [1, 2, 3], // uses IN + 'description' => null, // uses IS NULL +]); +// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL +``` + +For negative conditions, use the `NOT` operator: + +```php +$database->query('SELECT * FROM products WHERE', [ + 'name NOT' => 'Laptop', // uses the <> operator + 'category_id NOT' => [1, 2, 3], // uses NOT IN + 'description NOT' => null, // uses IS NOT NULL + 'id' => [], // skipped +]); +// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +``` + +By default, conditions are joined using the `AND` operator. This can be changed using the [?or placeholder |#SQL Construction Hints]. + + +ORDER BY Rules +-------------- + +The `ORDER BY` clause can be written using an array. Specify columns in the keys, and use a boolean value to indicate ascending (`true`) or descending (`false`) order: + +```php +$database->query('SELECT id FROM author ORDER BY', [ + 'id' => true, // ascending + 'name' => false, // descending +]); +// SELECT id FROM author ORDER BY `id`, `name` DESC +``` + + +Inserting Data (INSERT) +----------------------- + +The SQL `INSERT` command is used for inserting records. + +```php +$values = [ + 'name' => 'John Doe', + 'email' => 'john@example.com', +]; +$database->query('INSERT INTO users ?', $values); +$userId = $database->getInsertId(); +``` + +The `getInsertId()` method returns the ID of the last inserted row. For some databases (e.g., PostgreSQL), it is necessary to specify the name of the sequence from which the ID should be generated as a parameter, using `$database->getInsertId($sequenceId)`. + +You can also pass [#special values], such as files, DateTime objects, or enum types, as parameters. + +Inserting multiple records at once: + +```php +$database->query('INSERT INTO users ?', [ + ['name' => 'User 1', 'email' => 'user1@mail.com'], + ['name' => 'User 2', 'email' => 'user2@mail.com'], +]); +``` + +A multi-record INSERT is much faster because only a single database query is executed, instead of many individual ones. + +**Security Note:** Never use unvalidated data as `$values`. Familiarize yourself with [possible risks |security#Safe Work with Columns]. + + +Updating Data (UPDATE) +---------------------- + +The SQL `UPDATE` command is used for updating records. + +```php +// Update a single record +$values = [ + 'name' => 'John Smith', +]; +$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); +``` + +The number of affected rows is returned by `$result->getRowCount()`. + +For `UPDATE`, we can use the `+=` and `-=` operators: + +```php +$database->query('UPDATE users SET ? WHERE id = ?', [ + 'login_count+=' => 1, // increment login_count +], 1); +``` + +Example of inserting or updating a record if it already exists. We use the `ON DUPLICATE KEY UPDATE` technique: + +```php +$values = [ + 'name' => $name, + 'year' => $year, +]; +$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', + $values + ['id' => $id], + $values, +); +// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) +// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 +``` + +Notice that Nette Database recognizes the context in which an array parameter is used within the SQL command and constructs the SQL code accordingly. So, from the first array, it constructed `(id, name, year) VALUES (123, 'Jim', 1978)`, while it converted the second into the form `name = 'Jim', year = 1978`. We discuss this in more detail in the section [#SQL Construction Hints]. + + +Deleting Data (DELETE) +---------------------- + +The SQL `DELETE` command is used for deleting records. Example of obtaining the number of deleted rows: + +```php +$count = $database->query('DELETE FROM users WHERE id = ?', 1) + ->getRowCount(); +``` + + +SQL Construction Hints +---------------------- + +A hint is a special placeholder in an SQL query that specifies how the parameter value should be converted into an SQL expression: + +| Hint | Description | Automatically Used For +|-----------|-------------------------------------------------|----------------------------- +| `?name` | Used for inserting table or column names | - +| `?values` | Generates `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` +| `?set` | Generates assignments `key = value, ...` | `SET ?`, `KEY UPDATE ?` +| `?and` | Joins conditions in an array with `AND` | `WHERE ?`, `HAVING ?` +| `?or` | Joins conditions in an array with `OR` | - +| `?order` | Generates the `ORDER BY` clause | `ORDER BY ?`, `GROUP BY ?` + +The `?name` placeholder is used for dynamically inserting table and column names into the query. Nette Database handles the correct quoting of identifiers according to the database conventions (e.g., enclosing in backticks in MySQL). + +```php +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); +// SELECT `name` FROM `users` WHERE id = 1 (in MySQL) +``` + +**Warning:** Only use the `?name` placeholder for validated table and column names. Otherwise, you risk [security vulnerabilities |security#Dynamic Identifiers]. + +Other hints usually do not need to be specified, as Nette uses smart autodetection when constructing the SQL query (see the third column of the table). But you can use it, for example, in a situation where you want to join conditions using `OR` instead of `AND`: + +```php +$database->query('SELECT * FROM users WHERE ?or', [ + 'name' => 'John', + 'email' => 'john@example.com', +]); +// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' +``` + + +Special Values +-------------- + +In addition to common scalar types (string, int, bool), you can also pass special values as parameters: + +- files: `fopen('image.gif', 'r')` inserts the binary content of the file +- date and time: `DateTime` and `DateTimeImmutable` objects are converted to the database format +- enum types: instances of `enum` are converted to their value +- SQL literals: created using `Connection::literal('NOW()')` are inserted directly into the query + +```php +$database->query('INSERT INTO articles ?', [ + 'title' => 'My Article', + 'published_at' => new DateTimeImmutable, // or new DateTime + 'content' => fopen('image.png', 'r'), + 'state' => Status::Draft, +]); +``` + +For databases that do not have native support for the `datetime` data type (like SQLite and Oracle), `DateTime` and `DateTimeImmutable` objects are converted to a value specified in the [database configuration|configuration] by the `formatDateTime` item (default value is `U` - Unix timestamp). + + +SQL Literals +------------ + +In some cases, you need to pass raw SQL code as a value, which should not be treated as a string and escaped. Objects of the `Nette\Database\SqlLiteral` class are used for this purpose. They are created by the `Connection::literal()` method. + +```php +$result = $database->query('SELECT * FROM users WHERE', [ + 'name' => $name, + 'year >' => $database::literal('YEAR()'), +]); +// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) +``` + +Alternatively: + +```php +$result = $database->query('SELECT * FROM users WHERE', [ + 'name' => $name, + $database::literal('year > YEAR()'), +]); +// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) +``` + +SQL literals can contain parameters: + +```php +$result = $database->query('SELECT * FROM users WHERE', [ + 'name' => $name, + $database::literal('year > ? AND year < ?', $min, $max), +]); +// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) +``` + +This allows for interesting combinations: + +```php +$result = $database->query('SELECT * FROM users WHERE', [ + 'name' => $name, + $database::literal('?or', [ + 'active' => true, + 'role' => $role, + ]), +]); +// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') +``` + + +Fetching Data +============= + + +Shortcuts for SELECT Queries +---------------------------- + +To simplify data retrieval, `Connection` offers several shortcuts that combine a `query()` call with a subsequent `fetch*()` call. These methods accept the same parameters as `query()`, i.e., an SQL query and optional parameters. A full description of the `fetch*()` methods can be found [below |#fetch]. + +| `fetch($sql, ...$params): ?Row` | Executes the query and returns the first row as a `Row` object or `null`. +| `fetchAll($sql, ...$params): array` | Executes the query and returns all rows as an array of `Row` objects. +| `fetchPairs($sql, ...$params): array` | Executes the query and returns an associative array (key => value pairs). +| `fetchField($sql, ...$params): mixed` | Executes the query and returns the value of the first column in the first row. +| `fetchList($sql, ...$params): ?array` | Executes the query and returns the first row as an indexed array or `null`. + +Example: + +```php +// fetchField() - returns the value of the first cell +$count = $database->query('SELECT COUNT(*) FROM articles') + ->fetchField(); +``` + + +`foreach` - Iterating Over Rows +------------------------------- + +After executing a query, a [ResultSet|api:Nette\Database\ResultSet] object is returned, which allows iterating through the results in several ways. The easiest way to execute a query and retrieve rows is by iterating in a `foreach` loop. This method is the most memory-efficient, as it fetches data row by row and does not load the entire result set into memory at once. + +```php +$result = $database->query('SELECT * FROM users'); + +foreach ($result as $row) { + echo $row->id; + echo $row->name; + // ... +} +``` + +.[note] +The `ResultSet` can only be iterated once. If you need to iterate repeatedly, you must first load the data into an array, for example, using the `fetchAll()` method. + + +fetch(): ?Row .[method] +----------------------- + +Returns a row as a `Row` object. If no more rows exist, it returns `null`. Advances the internal pointer to the next row. + +```php +$result = $database->query('SELECT * FROM users'); +$row = $result->fetch(); // loads the first row +if ($row) { + echo $row->name; +} +``` + + +fetchAll(): array .[method] +--------------------------- + +Returns all remaining rows from the `ResultSet` as an array of `Row` objects. + +```php +$result = $database->query('SELECT * FROM users'); +$rows = $result->fetchAll(); // loads all rows +foreach ($rows as $row) { + echo $row->name; +} +``` + + +fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] +--------------------------------------------------------------------------------------- + +Returns the result set as an associative array. The first argument specifies the column to use as keys, and the second argument specifies the column to use as values: + +```php +$result = $database->query('SELECT id, name FROM users'); +$names = $result->fetchPairs('id', 'name'); +// [1 => 'John Doe', 2 => 'Jane Doe', ...] +``` + +If only the first parameter (`$key`) is provided, the entire row (`Row` object) will be used as the value: + +```php +$rows = $result->fetchPairs('id'); +// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] +``` + +In case of duplicate keys, the value from the last row is used. Using `null` as the key results in a numerically indexed array (starting from zero), preventing key collisions: + +```php +$names = $result->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] +``` + + +fetchPairs(Closure $callback): array .[method] +---------------------------------------------- + +Alternatively, you can provide a callback that processes each row. The callback can return a single value or a key-value pair. + +```php +$result = $database->query('SELECT * FROM users'); +$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); +// ['1 - John', '2 - Jane', ...] + +// The callback can also return an array with a key & value pair: +$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); +// ['John' => 46, 'Jane' => 21, ...] +``` + + +fetchField(): mixed .[method] +----------------------------- + +Returns the value of the first column from the current row. If no more rows exist, it returns `null`. Advances the internal pointer to the next row. + +```php +$result = $database->query('SELECT name FROM users'); +$name = $result->fetchField(); // loads the name from the first row +``` + + +fetchList(): ?array .[method] +----------------------------- + +Returns the row as an indexed array. If no more rows exist, it returns `null`. Advances the internal pointer to the next row. + +```php +$result = $database->query('SELECT name, email FROM users'); +$row = $result->fetchList(); // ['John', 'john@example.com'] +``` + + +getRowCount(): ?int .[method] +----------------------------- + +Returns the number of affected rows from the last `UPDATE` or `DELETE` query. For `SELECT` queries, it returns the number of rows in the result set. However, this might not always be known, in which case the method returns `null`. + + +getColumnCount(): ?int .[method] +-------------------------------- + +Returns the number of columns in the `ResultSet`. + + +Query Information +================= + +For debugging purposes, we can obtain information about the last executed query: + +```php +echo $database->getLastQueryString(); // prints the SQL query + +$result = $database->query('SELECT * FROM articles'); +echo $result->getQueryString(); // prints the SQL query +echo $result->getTime(); // prints the execution time in seconds +``` + +To display the result as an HTML table, you can use: + +```php +$result = $database->query('SELECT * FROM articles'); +$result->dump(); +``` + +`ResultSet` provides information about column types: + +```php +$result = $database->query('SELECT * FROM articles'); +$types = $result->getColumnTypes(); + +foreach ($types as $column => $type) { + echo "$column is of type $type->type"; // e.g., 'id is of type int' +} +``` + + +Query Logging +------------- + +We can implement custom query logging. The `onQuery` event is an array of callbacks that are called after each executed query: + +```php +$database->onQuery[] = function ($database, $result) use ($logger) { + $logger->info('Query: ' . $result->getQueryString()); + $logger->info('Time: ' . $result->getTime()); + + if ($result->getRowCount() > 1000) { + $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); + } +}; +``` diff --git a/database/en/transactions.texy b/database/en/transactions.texy new file mode 100644 index 0000000000..36b455a663 --- /dev/null +++ b/database/en/transactions.texy @@ -0,0 +1,43 @@ +Transactions +************ + +.[perex] +Transactions guarantee that either all operations within the transaction are executed, or none are. They are useful for ensuring data consistency during complex operations. + +The simplest way to use transactions looks like this: + +```php +$database->beginTransaction(); +try { + $database->query('DELETE FROM articles WHERE id = ?', $id); + $database->query('INSERT INTO audit_log', [ + 'article_id' => $id, + 'action' => 'delete' + ]); + $database->commit(); +} catch (\Exception $e) { + $database->rollBack(); + throw $e; +} +``` + +You can achieve the same result much more elegantly using the `transaction()` method. It accepts a callback which is executed within the transaction. If the callback runs without an exception, the transaction is automatically committed. If an exception occurs, the transaction is rolled back, and the exception is propagated further. + +```php +$database->transaction(function ($database) use ($id) { + $database->query('DELETE FROM articles WHERE id = ?', $id); + $database->query('INSERT INTO audit_log', [ + 'article_id' => $id, + 'action' => 'delete' + ]); +}); +``` + +The `transaction()` method can also return values: + +```php +$count = $database->transaction(function ($database) { + $result = $database->query('UPDATE users SET active = ?', true); + return $result->getRowCount(); // returns the number of updated rows +}); +``` diff --git a/database/en/upgrading.texy b/database/en/upgrading.texy new file mode 100644 index 0000000000..7bf5fa7f43 --- /dev/null +++ b/database/en/upgrading.texy @@ -0,0 +1,14 @@ +Upgrading +********* + + +Migrating from 3.1 to 3.2 +========================= + +The minimum required PHP version is 8.1. + +The code has been carefully tuned for PHP 8.1. All new type hints for methods and properties have been added. The changes are minor: + +- MySQL: zero date `0000-00-00` is returned as `null` +- MySQL: decimal without decimal places is returned as int instead of float +- The `time` type is returned as a `DateTimeImmutable` object with the date set to `0001-01-01` instead of the current date diff --git a/database/files/db-schema-1-.webp b/database/files/db-schema-1-.webp index 29905706c0..6bd9b0598d 100644 Binary files a/database/files/db-schema-1-.webp and b/database/files/db-schema-1-.webp differ diff --git a/database/meta.json b/database/meta.json index 248e88956e..76aebcf019 100644 --- a/database/meta.json +++ b/database/meta.json @@ -1,5 +1,5 @@ { - "version": "4.0", + "version": "3.x", "repo": "nette/database", "composer": "nette/database" } diff --git a/dependency-injection/cs/@home.texy b/dependency-injection/cs/@home.texy deleted file mode 100644 index 3e66ae0454..0000000000 --- a/dependency-injection/cs/@home.texy +++ /dev/null @@ -1,22 +0,0 @@ -Dependency Injection -******************** - -.[perex] -Dependency Injection je návrhový vzor, který zásadně změní váš pohled na kód a vývoj. Otevře vám cestu do světa čistě navržených a udržitelných aplikací. - -- [Co je Dependency Injection? |introduction] -- [Co je DI kontejner? |container] -- [Předávání závislostí |passing-dependencies] - - -Nette DI --------- - -Balíček `nette/di` poskytuje nesmírně pokročilý kompilovaný DI kontejner pro PHP. - -- [Nette DI Container |nette-container] -- [Konfigurace |configuration] -- [Definování služeb |services] -- [Autowiring |autowiring] -- [Generované továrny |factory] -- [Tvorba rozšíření pro Nette DI|extensions] diff --git a/dependency-injection/cs/@left-menu.texy b/dependency-injection/cs/@left-menu.texy deleted file mode 100644 index f4bade0b57..0000000000 --- a/dependency-injection/cs/@left-menu.texy +++ /dev/null @@ -1,15 +0,0 @@ -Dependency Injection -******************** -- [Co je DI? |introduction] -- [Co je DI kontejner? |container] -- [Předávání závislostí |passing-dependencies] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Konfigurace |configuration] -- [Definování služeb |services] -- [Autowiring |autowiring] -- [Generované továrny |factory] -- [Tvorba rozšíření pro Nette DI|extensions] diff --git a/dependency-injection/cs/autowiring.texy b/dependency-injection/cs/autowiring.texy deleted file mode 100644 index 3b2b4f8e0a..0000000000 --- a/dependency-injection/cs/autowiring.texy +++ /dev/null @@ -1,260 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring je skvělá vlastnost, která umí automaticky předávat do konstruktoru a dalších metod požadované služby, takže je nemusíme vůbec psát. Ušetří vám spoustu času. - -Díky tomu můžeme vynechat naprostou většinu argumentů při psaní definic služeb. Místo: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Stačí napsat: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring se řídí podle typů, takže aby fungoval, musí být třída `ArticleRepository` definována asi takto: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Aby bylo možné použit autowiring, musí pro každý typ být v kontejneru **právě jedna služba**. Pokud by jich bylo víc, autowiring by nevěděl, kterou z nich předat a vyhodil by výjimku: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # VYHODÍ VÝJIMKU, vyhovuje mainDb i tempDb -``` - -Řešením by bylo buď autowiring obejít a explicitně uvést název služby (tj `articles: Model\ArticleRepository(@mainDb)`). Šikovnější ale je autowirování jedné ze služeb [vypnout|#Vypnutí autowiringu], nebo první službu [upřednostnit|#Preference autowiringu]. - - -Vypnutí autowiringu -------------------- - -Autowirování služby můžeme vypnout pomocí volby `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # služba tempDb je vyřazena z autowiringu - - articles: Model\ArticleRepository # tudíž předá do kontruktoru mainDb -``` - -Služba `articles` nevyhodí výjimku, že existují dvě vyhovující služby typu `PDO` (tj. `mainDb` a `tempDb`), které lze do konstruktoru předat, protože vidí jen službu `mainDb`. - -.[note] -Konfigurace autowiringu v Nette funguje odlišně než v Symfony, kde volba `autowire: false` říká, že se nemá autowiring používat pro argumenty konstruktoru dané služby. -V Nette se autowiring používá vždy, ať už pro argumenty konstruktoru, nebo kterékoliv jiné metody. Volba `autowired: false` říká, že instance dané služba nemá být pomocí autowiringu nikam předávána. - - -Preference autowiringu ----------------------- - -Pokud máme více služeb stejného typu a u jedné z nich uvedeme volbu `autowired`, stává se tato služba preferovanou: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # stává se preferovanou - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Služba `articles` nevyhodí výjimku, že existují dvě vyhovující služby typu `PDO` (tj. `mainDb` a `tempDb`), ale použije preferovanou službu, tedy `mainDb`. - - -Pole služeb ------------ - -Autowiring umí předávat i pole služeb určitého typu. Protože v PHP nelze nativně zapsat typ položek pole, je třeba kromě typu `array` doplnit i phpDoc komentář s typem položky ve tvaru `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -DI kontejner pak automaticky předá pole služeb odpovídajících danému typu. Vynechá služby, které mají vypnutý autowiring. - -Pokud nemůžete ovlivnit podobu phpDoc komentáře, můžete předat pole služeb přímo v konfiguraci pomocí [`typed()`|services#Speciální funkce]. - - -Skalární argumenty ------------------- - -Autowiring umí dosazovat pouze objekty a pole objektů. Skalární argumenty (např. řetězce, čísla, booleany) [zapíšeme v konfiguraci |services#Argumenty]. -Alternativnou je vytvořit [settings-objekt |best-practices:passing-settings-to-presenters], který skalární hodnotu (nebo více hodnot) zapouzdří do podoby objektu, a ten pak lze opět předávat pomocí autowiringu. - -```php -class MySettings -{ - public function __construct( - // readonly je možné použít od PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Vytvoříte z něj službu přidáním do konfigurace: - -```neon -services: - - MySettings('any value') -``` - -Všechny třídy si jej poté vyžádají pomocí autowiringu. - - -Zúžení autowiringu ------------------- - -Jednotlivým službám lze autowiring zúžit jen na určité třídy nebo rozhraní. - -Normálně autowiring službu předá do každého parametru metody, jehož typu služba odpovídá. Zúžení znamená, že stanovíme podmínky, kterým musí typy uvedené u parametrů metod vyhovovat, aby jim byla služba předaná. - -Ukážeme si to na příkladu: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Pokud bychom je všechny zaregistrovali jako služby, tak by autowiring selhal: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # VYHODÍ VÝJIMKU, vyhovují služby parent i child - childDep: ChildDependent # autowiring předá do konstruktoru službu child -``` - -Služba `parentDep` vyhodí výjimku `Multiple services of type ParentClass found: parent, child`, protože do jejího kontruktoru pasují obě služby `parent` i `child`, a autowiring nemůže rozhodnout, kterou z nich zvolit. - -U služby `child` můžeme proto zúžit její autowirování na typ `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # lze napsat i 'autowired: self' - - parentDep: ParentDependent # autowiring předá do konstruktoru službu parent - childDep: ChildDependent # autowiring předá do konstruktoru službu child -``` - -Nyní se do kontruktoru služby `parentDep` předá služba `parent`, protože teď je to jediný vyhovující objekt. Službu `child` už tam autowiring nepředá. Ano, služba `child` je stále typu `ParentClass`, ale už neplatí zužující podmínka daná pro typ parametru, tj. neplatí, že `ParentClass` *je nadtyp* `ChildClass`. - -U služby `child` by bylo možné `autowired: ChildClass` zapsat také jako `autowired: self`, jelikož `self` je zástupné označení pro třídu aktuální služby. - -V klíči `autowired` je možné uvést i několik tříd nebo interfaců jako pole: - -```neon -autowired: [BarClass, FooInterface] -``` - -Zkusme příklad doplnit ještě o rozhraní: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Když službu `child` nijak neomezíme, bude pasovat do konstruktorů všech tříd `FooDependent`, `BarDependent`, `ParentDependent` i `ChildDependent` a autowiring ji tam předá. - -Pokud její autowiring ale zúžíme na `ChildClass` pomocí `autowired: ChildClass` (nebo `self`), předá ji autowiring pouze do konstruktoru `ChildDependent`, protože vyžaduje argument typu `ChildClass` a platí, že `ChildClass` *je typu* `ChildClass`. Žádný další typ uvedený u dalších parametrů není nadtypem `ChildClass`, takže se služba nepředá. - -Pokud jej omezíme na `ParentClass` pomocí `autowired: ParentClass`, předá ji autowiring opět do konstruktoru `ChildDependent` (protože vyžadovaný `ChildClass` je nadtyp `ParentClass` a nově i do konstruktoru `ParentDependent`, protože vyžadovaný typ `ParentClass` je taktéž vyhovující. - -Pokud jej omezíme na `FooInterface`, bude stále autowirovaná do `ParentDependent` (vyžadovaný `ParentClass` je nadtyp `FooInterface`) a `ChildDependent`, ale navíc i do konstruktoru `FooDependent`, nikoliv však do `BarDependent`, neboť `BarInterface` není nadtyp `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring předá do konstruktoru child - barDep: BarDependent # VYHODÍ VÝJIMKU, žádná služba nevyhovuje - parentDep: ParentDependent # autowiring předá do konstruktoru child - childDep: ChildDependent # autowiring předá do konstruktoru child -``` diff --git a/dependency-injection/cs/configuration.texy b/dependency-injection/cs/configuration.texy deleted file mode 100644 index a42f461c3c..0000000000 --- a/dependency-injection/cs/configuration.texy +++ /dev/null @@ -1,317 +0,0 @@ -Konfigurace DI kontejneru -************************* - -.[perex] -Přehled konfiguračních voleb pro Nette DI kontejner. - - -Konfigurační soubor -=================== - -Nette DI kontejner se snadno ovládá pomocí konfiguračních souborů. Ty se obvykle zapisují ve [formátu NEON|neon:format]. K editaci doporučujeme [editory s podporou|best-practices:editors-and-tools#ide-editor] tohoto formátu. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Dekorátor .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI kontejner .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Rozšíření]: "Instalace dalších DI rozšíření .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Vkládání souborů]: "Vkládání souborů .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#parametry]: "Parametry .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Automatická registrace služeb .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Služby .[prism-token prism-comment]" -</pre> - -Chcete-li zapsat řetězec obsahující znak `%`, musíte jej escapovat zdvojením na `%%`. .[note] - - -Parametry -========= - -V konfiguraci můžete definovat parametry, které lze pak použít jako součást definic služeb. Čímž můžete zpřehlednit konfiguraci nebo sjednotit a vyčlenit hodnoty, které se budou měnit. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Na parametr `dsn` se odkážeme kdekoliv v konfiguraci zápisem `%dsn%`. Parametry lze používat i uvnitř řetězců jako `'%wwwDir%/images'`. - -Parametry nemusí být jen řetězce nebo čísla, mohou také obsahovat pole: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Na konkrétní klíč se odkážeme jako `%mailer.user%`. - -Pokud potřebujete ve vašem kódu, například třídě, zjistit hodnotu jakékoliv parametru, tak jej do této třídy předejte. Například v konstruktoru. Neexistuje žádný globální objekt představující konfiguraci, kterého by se třídy dotazovaly na hodnoty parametrů. To by bylo porušením principu dependency injection. - - -Služby -====== - -Viz [samostatná kapitola|services]. - - -Decorator -========= - -Jak upravit hromadně všechny služby určitého typu? Třeba zavolat určitou metodu u všech presenterů, které dědí od konkrétního společného předka? Od toho je tu decorator. - -```neon -decorator: - # u všech služeb, co jsou instancí této třídy nebo rozhraní - App\Presenters\BasePresenter: - setup: - - setProjectId(10) # zavolej tuto metodu - - $absoluteUrls = true # a nastav proměnnou -``` - -Decorator se dá používat také pro nastavení [tagů|services#Tagy] nebo zapnutí režimu [inject|services#Režim Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Technické nastavení DI kontejneru. - -```neon -di: - # zobrazit DIC v Tracy Bar? - debugger: ... # (bool) výchozí je true - - # typy parametrů, které nikdy neautowirovat - excluded: ... # (string[]) - - # třída, od které dědí DI kontejner - parentClass: ... # (string) výchozí je Nette\DI\Container -``` - - -Export metadat --------------- - -Třída DI kontejneru obsahuje i spoustu metadat. Můžete ji zmenšit tím, že export metadat zredukujete. - -```neon -di: - export: - # exportovat parametry? - parameters: false # (bool) výchozí je true - - # exportovat tagy a které? - tags: # (string[]|bool) výchozí jsou všechny - - event.subscriber - - # exportovat data pro autowiring a které? - types: # (string[]|bool) výchozí jsou všechny - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Pokud nevyužíváte pole `$container->parameters`, můžete vypnout export parametrů. Dále můžete exportovat jen ty tagy, přes které získáváte služby metodou `$container->findByTag(...)`. -Pokud metodu nevoláte vůbec, můžete zcela vypnout export tagů pomocí `false`. - -Výrazně můžete zredukovat metadata pro [autowiring] tím, že uvedete třídy, které používáte jako parametr metody `$container->getByType()`. -A opět, pokud metodu nevoláte vůbec (resp. jen v [bootstrapu|application:bootstrap] pro získání `Nette\Application\Application`), můžete export úplně vypnout pomocí `false`. - - -Rozšíření -========= - -Registrace dalších DI rozšíření. Tímto způsobem přidáme např. DI rozšíření `Dibi\Bridges\Nette\DibiExtension22` pod názvem `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Následně ho tedy konfigurujeme v sekci `dibi`: - -```neon -dibi: - host: localhost -``` - -Jako rozšíření lze přidat i třídu, která má parametry: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Vkládání souborů -================ - -Další konfigurační soubory můžeme vložit v sekci `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Název `parameters.php` není překlep, konfigurace může být zapsaná také v PHP souboru, který ji vrátí jako pole: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Pokud se v konfiguračních souborech objeví prvky se stejnými klíči, budou přepsány, nebo v případě [polí sloučeny |#Slučování]. Později vkládaný soubor má vyšší prioritu než předchozí. Soubor, ve kterém je sekce `includes` uvedena, má vyšší prioritu než v něm vkládané soubory. - - -Search -====== - -Automatické přidávání služeb do DI kontejneru nesmírně zpříjemňuje práci. Nette automaticky přidává do kontejneru presentery, lze však snadno přidávat i jakékoliv jiné třídy. - -Stačí uvést, ve kterých adresářích (a podadresářích) má třídy hledat: - -```neon -search: - # názvy sekcí si volíte sami - formuláře: - in: %appDir%/Forms - - model: - in: %appDir%/Model -``` - -Obvykle ovšem nechceme přidávat úplně všechny třídy a rozhraní, proto je můžeme filtrovat: - -```neon -search: - formuláře: - in: %appDir%/Forms - - # filtrování podle názvu souboru (string|string[]) - files: - - *Factory.php - - # filtrování podle názvu třídy (string|string[]) - classes: - - *Factory -``` - -Nebo můžeme vybírat třídy, které dědí či implementují alespoň jednu z uvedených tříd: - - -```neon -search: - formuláře: - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Lze definovat i vylučující pravidla, tj. masky názvu třídy nebo dědičné předky, které pokud vyhovují, služba se do DI kontejneru nepřidá: - -```neon -search: - formuláře: - exclude: - classes: ... - extends: ... - implements: ... -``` - -Všem službám lze nastavit tagy: - -```neon -search: - formuláře: - tags: ... -``` - - -Slučování -========= - -Pokud se ve více konfiguračních souborech objeví prvky se stejnými klíči, budou přepsány, nebo v případě polí sloučeny. Později vkládaný soubor má vyšší prioritu než předchozí. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>výsledek</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -U polí lze zabránit slučování uvedením vykřičníku za názvem klíče: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>výsledek</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Konfigurace Dependency Injection}} diff --git a/dependency-injection/cs/container.texy b/dependency-injection/cs/container.texy deleted file mode 100644 index 5108de5ed7..0000000000 --- a/dependency-injection/cs/container.texy +++ /dev/null @@ -1,145 +0,0 @@ -Co je DI kontejner? -******************* - -.[perex] -Dependency injection kontejner (DIC) je třída, která umí instancovat a konfigurovat objekty. - -Možná vás to překvapí, ale v mnoha případech nepotřebujete dependency injection kontejner, abyste mohli využívat výhod dependency injection (krátce DI). Vždyť i v [předchozí kapitole|introduction] jsme si na konkrétních příkladech DI ukázali a žádný kontejner nebyl potřeba. - -Pokud však potřebujete spravovat velké množství různých objektů s mnoha závislostmi, bude dependency injection container opravdu užitečný. Což je třeba případ webových aplikací postavených na frameworku. - -V předchozí kapitole jsme si představili třídy `Article` a `UserController`. Obě mají nějaké závislosti, a to databázi a továrnu `ArticleFactory`. A pro tyto třídy si nyní vytvoříme kontejner. Samozřejmě pro tak jednoduchý příklad nemá smysl mít kontejner. Ale vytvoříme ho, abychom si ukázali, jak vypadá a funguje. - -Zde je jednoduchý hardcoded kontejner pro uvedený příklad: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createUserController(): UserController - { - return new UserController($this->createArticleFactory()); - } -} -``` - -Použití by vypadalo následovně: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Kontejneru se pouze zeptáme na objekt a již nemusíme vědět nic o tom, jak jej vytvořit a jaké má závislosti; to všechno ví kontejner. Závislosti jsou kontejnerem injektovány automaticky. V tom je jeho síla. - -Kontejner má zatím zapsané všechny údaje navrdo. Uděláme tedy další krok a přidáme parametry, aby byl kontejner skutečně užitečný: - -```php -class Container -{ - private array $parameters; - - public function __construct(array $parameters) - { - $this->parameters = $parameters; - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'] - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Bystří čtenáři si možná všimli jistého problému. Pokaždé, když získám objekt `UserController`, vytvoří se také nová instance `ArticleFactory` a databáze. To rozhodně nechceme. - -Přidáme proto metodu `getService()`, která bude vracet stále stejné instance: - -```php -class Container -{ - private array $parameters; - private array $services = []; - - public function __construct(array $parameters) - { - $this->parameters = $parameters; - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') bude volat createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Při prvním volání např. `$container->getService('Database')` si nechá od `createDatabase()` vytvořit objekt databáze, který uloží do pole `$services` a při příštím volání jej rovnou vrátí. - -Upravíme i zbytek kontejneru, aby používal `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Mimochodem, termínem služba se označuje jakýkoliv objekt spravovaný kontejnerem. Proto i ten název metody `getService()`. - -Hotovo. Máme plně funkční DI kontejner! A můžeme ho použít: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Jak vidíte, napsat DIC není nic složitého. Za připomenutí stojí, že samotné objekty neví, že je vytváří nějaký kontejner. Tím pádem je možné takto vytvářet jakýkoliv objekt v PHP bez zásahu do jeho zdrojového kódu. - -Ruční vytváření a údržba třídy kontejneru se může poměrně rychle stát noční můrou. V další kapitole si proto povíme o [Nette DI Containeru|nette-container], který se umí generovat a aktualizovat téměř sám. - - -{{maintitle: Co je dependency injection kontejner?}} diff --git a/dependency-injection/cs/extensions.texy b/dependency-injection/cs/extensions.texy deleted file mode 100644 index 873663753d..0000000000 --- a/dependency-injection/cs/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Tvorba rozšíření pro Nette DI -***************************** - -.[perex] -Generování DI kontejneru kromě konfiguračních souborů ovlivňují ještě tzv *rozšíření*. Aktivujeme je v konfiguračním souboru v sekci `extensions`. - -Takto přidáme rozšíření reprezentované třídou `BlogExtension` pod názvem `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Každé rozšíření kompileru dědí od [api:Nette\DI\CompilerExtension] a může implementovat následující metody, které jsou postupně volány během sestavování DI kontejneru: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Tato metoda se volá jako první. Definuje schema pro validaci konfiguračních parametrů. - -Rozšíření konfigurujeme v sekci, jejíž název je stejný jako ten, pod kterým bylo rozšíření přidáno, tedy `blog`: - -```neon -# stejné jméno jako má extension -blog: - postsPerPage: 10 - allowComments: false -``` - -Vytvoříme schema popisující všechny konfigurační volby včetně jejich typů, povolených hodnot a případně i výchozích hodnot: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), - ]); - } -} -``` - -Dokumentaci najdete na stránce [Schema |schema:]. Navíc lze určit, které volby mohou být [dynamické|application:bootstrap#Dynamické parametry] pomocí `dynamic()`, např. `Expect::int()->dynamic()`. - -Ke konfiguraci se dostaneme přes proměnnou `$this->config`, což je objekt `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Používá se přidání služeb do kontejneru. K tomu slouží [api:Nette\DI\ContainerBuilder]: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // or setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -Konvence je prefixovat služby přidané rozšířením jeho názvem, aby nevznikaly jmenné konflikty. To dělá metoda `prefix()`, takže pokud se rozšíření jmenuje `blog`, služba ponese název `blog.articles`. - -Pokud potřebujeme přejmenovat službu, můžeme kvůli zachování zpětné kompatibility vytvořit alias s původním názvem. Podobně to dělá Nette např. u služby `routing.router`, která je dostupná i pod dřívějším názvem `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Načtení služeb ze souboru -------------------------- - -Služby nemusíme vytvářet jen pomocí API třídy ContainerBuilder, ale i známým zápisem používaným v konfiguračním souboru NEON v sekci services. Prefix `@extension` představuje aktuální extension. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Služby načteme: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // načtení konfiguračního souboru pro rozšíření - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Metoda se volá ve chvíli, kdy kontejner obsahuje všechny služby přidané jednotlivými rozšířeními v metodách `loadConfiguration` a taktéž uživatelskými konfiguračními soubory. V této fázi sestavování tedy můžeme definice služeb upravovat nebo doplnit vazby mezi nimi. Pro vyhledávání služeb v kontejneru podle tagů lze využít metodu `findByTag()`, podle třídy či rozhraní zase metodu `findByType()`. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } - } -} -``` - - -afterCompile() .[method] -======================== - -V této fázi už je třída kontejneru vygenerována v podobě objektu [ClassType |php-generator:#tridy], obsahuje všechny metody, které vytváří služby, a je připravena na zápis do cache. Výsledný kód třídy můžeme v této chvíli ještě upravit. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[wiki-method] -============================== - -Třída Configurator po [vytvoření kontejneru |application:bootstrap#index.php] volá inicializační kód, který se vytváří zápisem do objektu `$this->initialization` pomocí [metody addBody() |php-generator:#tela-metod-a-funkci]. - -Ukážeme si příklad, jak třeba inicializačním kódem nastartovat session nebo spustit služby, které mají tag `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // automatické startování session - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // služby s tagem run musejí být vytvořeny po instancování kontejneru - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/cs/factory.texy b/dependency-injection/cs/factory.texy deleted file mode 100644 index 691124a557..0000000000 --- a/dependency-injection/cs/factory.texy +++ /dev/null @@ -1,228 +0,0 @@ -Generované továrny -****************** - -.[perex] -Nette DI umí automaticky generovat kód továren na základě rozhraní, což vám ušetří psaní kódu. - -Továrna je třída, která vyrábí a konfiguruje objekty. Předává jim tedy i jejich závislosti. Jak taková továrna vypadá jsme si ukázali v [úvodní kapitole|introduction#továrna]: - -```php -class ArticleFactory -{ - private Nette\Database\Connection $db; - - public function __construct(Nette\Database\Connection $db) - { - $this->db = $db; - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI umí kód továren automaticky generovat. Vše, co musíte udělat, je vytvořit rozhraní a Nette DI vygeneruje implementaci. Rozhraní musí mít přesně jednu metodu s názvem `create` a deklarovat návratový typ: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Tedy továrna `ArticleFactory` má metodu `create`, která vytváří objekty `Article`. Třída `Article` může vypadat třeba následovně: - -```php -class Article -{ - private $db; - - public function __construct(Nette\Database\Connection $db) - { - $this->db = $db; - } -} -``` - -Továrnu přidáme do konfiguračního souboru: - -```neon -services: - - ArticleFactory -``` - -Nette DI vygeneruje odpovídající implementaci továrny. - -V kódu, který továrnu používá, si tak vyžádáme objekt podle rozhraní a Nette DI použije vygenerovanou implementaci: - -```php -class UserController -{ - private $articleFactory; - - public function __construct(ArticleFactory $articleFactory) - { - $this->articleFactory = $articleFactory; - } - - public function foo() - { - // necháme továrnu vytvořit objekt - $article = $this->articleFactory->create(); - } -} -``` - - -Parametrizovaná továrna -======================= - -Tovární metoda `create` může přijímat parametry, které poté předá do konstrukturu. Doplňme například třídu `Article` o ID autora článku: - -```php -class Article -{ - private $db; - private $authorId; - - public function __construct(Nette\Database\Connection $db, int $authorId) - { - $this->db = $db; - $this->authorId = $authorId; - } -} -``` - -Parametr přidáme také do továrny: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Díky tomu, že se parametr v konstruktoru i parametr v továrně jmenují stejně, Nette DI je zcela automaticky předá. - - -Pokročilá definice -================== - -Definici lze zapsat i ve víceřádkové podobě za použití klíče `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Při zápisu tímto delším způsobem je možné uvést další argumenty pro konstruktor v klíči `arguments` a doplňující konfiguraci pomocí `setup`, stejně, jako u běžných služeb. - -Příklad: pokud by metoda `create()` nepřijímala parametr `$authorId`, mohli bychom uvést pevnou hodnotu v konfiguraci, která by se předávala do konstruktoru `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Nebo naopak pokud by `create()` parametr `$authorId` přijimala, ale nebyl by součástí konstruktoru a předával se metodou `Article::setAuthorId()`, odkázali bychom se na něj v sekci `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Nette umí krom továren generovat i tzv. accessory. Jde o objekty s metodou `get()`, která vrací určitou službu z DI kontejneru. Opakované volání `get()` vrací stále tutéž instanci. - -Accessor poskytují závislostem lazy-loading. Mějme třídu, která zapisuje chyby do speciální databáze. Když by si tato třída nechávala připojení k databázi předávat jako závislost konstruktorem, muselo by se připojení vždycky vytvořit, ačkoliv v praxi se chyba objeví jen výjimečně a tedy povětšinou by zůstalo spojení nevyužité. -Místo toho si tak třída předá accessor a teprve když se zavolá jeho `get()`, dojde k vytvoření objektu databáze: - -Jak accessor vytvořit? Stačí napsat rozhraní a Nette DI vygeneruje implementaci. Rozhraní musí mít přesně jednu metodu s názvem `get` a deklarovat návratový typ: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Accessor přidáme do konfiguračního souboru, kde je také definice služby, kterou bude vracet: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Protože accessor vrací službu typu `PDO` a v konfiguraci je jediná taková služba, bude vracet právě ji. Pokud by služeb daného typu bylo víc, určíme vracenou službu pomocí názvu, např. `- PDOAccessor(@db1)`. - - -Vícenásobná továrna/accessor -============================ -Naše továrny a accessory uměly zatím vždy vyrábět nebo vracet jen jeden objekt. Lze ale velmi snadno vytvořit i vícenásobné továrny kombinované s accessory. Rozhraní takové třídy bude obsahovat libovolný počet metod s názvy `create<name>()` a `get<name>()`, např.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function createFoo(): Model\Foo; - function getDb(): PDO; -} -``` - -Takže místo toho, abych si předávali několik generovaných továren a accessorů, předáme jednu komplexnější továrnu, která toho umí víc. - -Alternativně lze místo několika metod použít `create()` a `get()` s parameterem: - -```php -interface MultiFactoryAlt -{ - function create($name); - function get($name): PDO; -} -``` - -Pak platí, že `MultiFactory::createArticle()` dělá totéž jako `MultiFactoryAlt::create('article')`. Nicméně alternativní zápis má tu nevýhodu, že není zřejmé, jaké hodnoty `$name` jsou podporované a logicky také nelze v rozhraní odlišit různé návratové hodnoty pro různé `$name`. - - -Definice seznamem ------------------ -A jak definovat vícenásobnou továrnu v konfiguraci? Vytvoříme tři služby, které bude vytvářet/vracet, a potom samotnou továrnu: - -```neon -services: - article: Article - - Model\Foo - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # createArticle() - foo: @Model\Foo # createFoo() - db: @\PDO # getDb() - ) -``` - - -Definice pomocí tagů --------------------- - -Druhou možností je využít k definici [tagy|services#Tagy]: - -```neon -services: - - App\Router\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/cs/introduction.texy b/dependency-injection/cs/introduction.texy deleted file mode 100644 index 7a28913477..0000000000 --- a/dependency-injection/cs/introduction.texy +++ /dev/null @@ -1,469 +0,0 @@ -Co je Dependency Injection? -*************************** - -.[perex] -Tato kapitola vás seznámí se základními programátorskými postupy, na kterých stojí celý framework Nette a které byste měli dodržovat při psaní vlastních aplikací. Jde o základy nutné pro psaní čistého, srozumitelného a udržitelného kódu. - -Pokud si tyto pravidla osvojíte a budete je dodržovat, bude vám framework v každém kroku vycházet vstříc. Bude za vás řešit rutinní úlohy a poskytne vám maximální pohodlí, abyste se mohli soustředit na samotnou logiku. - -Principy, které si zde ukážeme, jsou přitom celkem prosté. Nemusíte se ničeho obávat. - - -Pamatujete na svůj první program? ---------------------------------- - -Netušíme sice, v jakém jazyce jste ho psali, ale kdyby to bylo PHP, nejspíš by vypadal nějak takto: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // vypíše 24 -``` - -Pár triviálních řádků kódu, ale přitom se v nich skrývá tolik klíčových konceptů. Že existují proměnné. Že se kód člení do menších jednotek, což jsou kupříkladu funkce. Že jim předáváme vstupní argumenty a ony vracejí výsledky. Chybí tam už jen podmínky a cykly. - -To, že funkci předáme vstupní data a ona vrátí výsledek, je perfektně srozumitelný koncept, který se používá i v jiných oborech, jako je třeba v matematice. - -Funkce má svoji signaturu, kterou tvoří její název, přehled parametrů a jejich typů, a nakonec typ návratové hodnoty. Jako uživatele nás zajímá signatura, o vnitřní implementaci obvykle nepotřebujeme nic vědět. - -Teď si představte, že by signatura funkce vypadala takto: - -```php -function soucet(float $x): float -``` - -Součet s jedním parametrem? To je divné… A co třeba takto? - -```php -function soucet(): float -``` - -Tak to už je opravdu hodně divné, že? Jak se funkce asi používá? - -```php -echo soucet(); // co asi vypíše? -``` - -Při pohledu na takový kód bychom byli zmateni. Nejen že by mu nerozuměl začátečník, takovému kódu nerozumí ani zdatný programátor. - -Přemýšlíte, jak by vlastně taková funkce vypadala uvnitř? Kde vezme sčítance? Zřejmě by si je *nějakým způsobem* obstarala sama, třeba takto: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -V těle funkce jsme objevili skryté vazby na další funkce (či statické metody) a abychom zjistili, odkud se skutečně sčítance berou, musíme pátrat dál. - - -Tudy ne! --------- - -Návrh, který jsme si právě ukázali, je esencí mnoha negativních rysů: - -- signatura funkce se tvářila, že nepotřebuje sčítance, což nás mátlo -- vůbec nevíme, jak přimět funkci sečíst jiná dvě čísla -- museli jsme se podívat do kódu, abychom zjistili, kde sčítance bere -- objevili jsme skryté vazby -- pro plné pochopení je třeba prozkoumat i tyto vazby - -A je vůbec úkolem sčítací funkce obstarávat si vstupy? Samozřejmě, že není. Její zodpovědností je pouze samotné sčítání. - -S takovým kódem se nechceme setkat, a rozhodně ho nechceme psát. Náprava je přitom jednoduchá: vrátit se k základům a prostě použít parametry: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Pravidlo č. 1: používej parametry ---------------------------------- - -Nejdůležitější pravidlo zní: **všechna data, která funkce nebo třídy potřebují, jim musíme předat**. - -Když tohle pravidlo porušíme, nebude možné dosáhnout toho, aby byl kód srozumitelný, čistý a dlouhodobě udržitelný. - -Když ho budeme dodržovat, jsme na cestě ke kódu bez skrytých vazeb. Ke kódu, který je srozumitelný nejen autorovi, ale i každému, kdo jej po něm bude číst. Kde je vše pochopitelné ze signatur funkcí a tříd a není třeba pátrat po skrytých tajemstvích v implementaci. - -Této technice předávání argumentů se odborně říká **dependency injection**. - -(Nezaměňujte dependency injection s „dependency injection container“, jde o něco diametrálně odlišného a kontejnerum se budeme věnovat v [druhé kapitole|container].) - - -Od funkcí ke třídám -------------------- - -A jak s tím souvisí třídy? Třída je komplexnější celek než jednoduchá funkce, nicméně pravidlo č. 1 platí bezezbytku i tady. Jen existuje [víc možností, jak argumenty předat|passing-dependencies]. Kupříkladu docela podobně jako v případě funkce: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Nebo pomocí jiných metod, či přímo konstruktoru: - -```php -class Soucet -{ - private float $a; - private float $b; - - public function __construct(float $a, float $b) - { - $this->a = $a; - $this->b = $b; - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Obě ukázky jsou zcela v souladu s dependency injection. - - -Reálné příklady ---------------- - -V reálném světe nebudete psát třídy pro sčítání čísel. Pojďme se přesunout k příkladům z praxe. - -Mějme třídu `Article` reprezentující článek na blogu: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // uložíme článek do databáze - } -} -``` - -a použití bude následující: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -Metoda `save()` uloží článek do databázové tabulky. Implementovat ji za pomoci [Nette Database |database:] bude hračka, nebýt jednoho zádrhelu: kde má `Article` vzít připojení k databázi, tj. objekt třídy `Nette\Database\Connection`? - -Zdá se, že máme spoustu možností. Může jej vzít odněkud ze statické proměnné. Nebo dědit od třídy, která spojení s databází zajistí. Nebo využít tzv. singletonu. Nebo tzv. facades, které se používají v Laravelu: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Skvělé, problém jsme vyřešili. - -Nebo ne? - -Připomeňme [#pravidlo č. 1: používej parametry]: všechna data, která třída potřebuje, jim musíme předat. Protože pokud to neuděláme a pravidlo porušíme, nastoupili jsme cestu ke špinavému kódu plného skrytých vazeb, nesrozumitelnosti, a výsledkem bude aplikace, kterou bude bolest udržovat a vyvíjet. - -Uživatel třídy `Article` netuší, kam metoda `save()` článek ukládá. Do databázové tabulky? Do které, ostré nebo testovací? A jak to lze změnit? - -Uživatel se musí podívat, jak je implementovaná metoda `save()`, kde najde použití metody `DB::insert()`. Takže musí pátrat dál, jak si tato metoda obstarává databázové spojení. A skryté vazby mohou tvořit docela dlouhý řetězec. - -V čistém a dobře navrženém kódu se nikdy nevyskytují skryté vazby, Laravelovské facades nebo statické proměnné. V čistém a dobře navrženém kódu se předávají argumenty: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Ještě praktičtější, jak uvidíme dále, to bude konstruktorem: - -```php -class Article -{ - private Nette\Database\Connection $db; - - public function __construct(Nette\Database\Connection $db) - { - $this->db = $db; - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Budete-li psát třídu vyžadující ke své činnosti např. databázi, nevymýšlejte, odkud ji získat, ale nechte si ji předat. Třeba jako parametr konstruktoru nebo jiné metody. Přiznejte závislosti. Přiznejte je v API vaší třídy. Získáte srozumitelný a předvídatelný kód. - -A co třeba tato třída, která loguje chybové zprávy: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Co myslíte, dodrželi jsme [#pravidlo č. 1: používej parametry]? - -Nedodrželi. - -Klíčovou informaci, tedy adresář se souborem s logem, si třída *obstarává sama* z konstanty. - -Podívejte se na příklad použití: - -```php -$logger = new Logger; -$logger->log('Teplota je 23 °C'); -$logger->log('Teplota je 10 °C'); -``` - -Bez znalosti implementace, dokázali byste zodpovědět otázku, kam se zprávy zapisují? Napadlo by vás, že pro fungování je potřeba existence konstanty LOG_DIR? A dokázali byste vytvořit druhou instanci, která bude zapisovat jinam? Určitě ne. - -Pojďme třídu opravit: - -```php -class Logger -{ - private string $file; - - public function __construct(string $file) - { - $this->file = $file; - } - - public function log(string $message) - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Třída je teď mnohem srozumitelnější, konfigurovatelnější a tedy užitečnější. - -```php -$logger = new Logger('/cesta/k/logu.txt'); -$logger->log('Teplota je 15 °C'); -``` - - -Ale to mě nezajímá! -------------------- - -*„Když vytvořím objekt Article a zavolám save(), tak nechci řešit databázi, prostě chci, aby se uložil do té kterou mám nastavenou v konfiguraci.“* - -*„Když použiju Logger, tak prostě chci, aby se zpráva zapsala, a nechci řešit kam. Ať se použije globální nastavení.“* - -To jsou správné připomínky. - -Jako příklad si ukážeme třídu rozesílající newslettery, která zaloguje, jak to dopadlo: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Emaily byly rozeslány'); - - } catch (Exception $e) { - $logger->log('Došlo k chybě při rozesílání'); - throw $e; - } - } -} -``` - -Jenže nový `Logger`, který již nepoužívá konstantu `LOG_DIR`, vyžaduje v konstruktoru uvést cestu k souboru. Jak tohle vyřešit? Třídu `NewsletterDistributor` vůbec nezajímá, kam se zprávy zapisují, chce je jen zapsat. - -Řešením je opět [#pravidlo č. 1: používej parametry]: všechna data, která třída potřebuje, jim předáme. - -Takže předáme do konstruktoru cestu k logu, kterou pak použijeme při vytváření objektu `Logger`? Nikoliv. Cesta totiž nejdou data, která třída `NewsletterDistributor` potřebuje; ty potřebuje `Logger`. Třída potřebuje logger jako takový. A ten si předáme: - - -```php -class NewsletterDistributor -{ - private Logger $logger; - - public function __construct(Logger $logger) - { - $this->logger = $logger; - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Emaily byly rozeslány'); - - } catch (Exception $e) { - $this->logger->log('Došlo k chybě při rozesílání'); - throw $e; - } - } -} -``` - -Nyní je ze signatur třídy `NewsletterDistributor` jasné, že součástí její funkčnosti je i logování. A máte možnost vyměnit logger za jiný. - -Zatímco v celé aplikaci si můžeme vystačit s jedinou instancí loggeru a předávat ji všude tam, kde se něco loguje, jinak je tomu v případě třídy `Article`. Její instance budeme chtít vytvářet vícekrát. Jak se vypořádat s vazbou na databázi v konstruktoru? Jako příklad si ukážeme kontroler, který po odeslání formuláře má uložit článek do databáze: - -```php -class UserController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Možné řešení se přímo nabízí: necháme si objekt databáze předat konstruktorem do `UserController` a použijeme `$article = new Article($this->db)`. - -Stejně jako v předchozím případě, tohle není správný postup. Databáze není závislost `UserController`, ale `Article`. Navíc ve chvíli, kdy se nějak změní konstruktor třídy `Article` (přibude nový parametr), budeme muset upravit i kód na všech místech, kde se vytváří instance. - -Řešením jsou továrny. - - -Pravidlo č. 2: používej továrny -------------------------------- - -Tím, že jsme zrušili skryté vazby a všechna data předáváme jako argumenty, získali jsme konfigurovatelnější a pružnější třídy. A proto potřebujeme ještě něco, co nám ty pružnější třídy vytvoří a nakonfiguruje. Budeme tomu říkat továrny. - -Pravidlo zní: pokud má třída závislosti, nech vytváření jejich instancí na továrně. - -Továrny jsou chytřejší náhrada operátoru `new` ve světě dependency injection. - - -Továrna -------- - -Továrna je třída, která vyrábí a konfiguruje objekty. Továrnu vyrábějící `Article` nazveme `ArticleFactory` a její použití v kontroleru bude následující: - -```php -class UserController extends Controller -{ - private ArticleFactory $articleFactory; - - public function __construct(ArticleFactory $articleFactory) - { - $this->articleFactory = $articleFactory; - } - - public function formSubmitted($data) - { - // necháme továrnu vytvořit objekt - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Implementace továrny může vypadat takto: - - -```php -class ArticleFactory -{ - private Nette\Database\Connection $db; - - public function __construct(Nette\Database\Connection $db) - { - $this->db = $db; - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Když se v tuto chvíli změní signatura konstruktoru třídy `Article`, jediná část kódu, která na to musí reagovat, je samotná továrna `ArticleFactory`. Veškerého dalšího kódu, který s objekty `Article` pracuje, jako například `UserController`, se to nijak nedotkne. - -Možná si teď klepete na čelo, jak jsme si to vlastně pomohli. Množství kódu narostlo a z controlleru se přesunulo do zvláštní třídy. Nette DI má však skryté eso v rukávu. Konceptu továren totiž rozumí a dokáže takovou službu dokonce [napsat za nás|factory]. Místo třídy `ArticleFactory` by tak stačílo vytvořit jen interface: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ale to teď trošku předbíháme, dostaneme se k tomu za chvíli. - - -Shrnutí -------- - -Na začátku této kapitoly jsme slibovali, že si ukážeme prostý princip, jak navrhovat aplikace. Ačkoliv princip samotný prostý je (předávej třídám data, které potřebují), to co z něj vyplývá už vyžaduje víc přemýšlení. Klidně si tuto kapitolu přečtěte několikrát. - -Programátoři, kteří zahodili staré zvyky a začali důsledně používat dependency injection, považují tento krok za zásadní moment v profesním životě. Otevřel se jim svět přehledných a udržitelných aplikací. - -Nyní si ukážeme, co je to [Dependency Injection Container|container]. diff --git a/dependency-injection/cs/nette-container.texy b/dependency-injection/cs/nette-container.texy deleted file mode 100644 index e48aa745d0..0000000000 --- a/dependency-injection/cs/nette-container.texy +++ /dev/null @@ -1,82 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI je jednou z nejzajímavějších knihoven Nette. Umí generovat a automaticky aktualizovat kompilované DI kontejnery, které jsou extrémně rychlé a úžasně snadno konfigurovatelné. - -Podobu služeb, které má vytvářet DI kontejner, definujeme obvykle pomocí konfiguračních souborů ve [formátu NEON|neon:format]. Kontejner, který jsme ručně vytvořili v [předchozí kapitole|container], by se zapsal takto: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Zápis je opravdu stručný. - -Všechny závislosti deklarované v konstruktorech tříd `ArticleFactory` a `UserController` si Nette DI samo zjistí a předá díky tzv. [autowiringu|autowiring], v konfiguračním souboru proto není potřeba nic uvádět. -Takže i když dojde ke změně parametrů, nemusíte v konfiguraci nic měnit. Nette kontejner automaticky přegeneruje. Vy se tam můžete soustředit čistě na vývoj aplikace. - -Pokud chceme závislosti předávat pomocí setterů, použijeme k tomu sekci [setup|services#setup]. - -Nette DI vygeneruje přímo PHP kód kontejneru. Výsledkem je tedy soubor `.php`, který si můžete otevřít a studovat. Díky tomu přesně vidíte, jak kontejner funguje. Můžete jej také debuggovat v IDE a krokovat. A hlavně: vygenerované PHP je extrémně rychlé. - -Nette DI umí také generovat kód [továren|factory] na základě dodaného rozhraní. Proto místo třídy `ArticleFactory` nám bude stačit vytvořit v aplikaci jen interface: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Celý příklad najdete [na GitHubu|https://github.com/nette-examples/di-example-doc]. - - -Samostatné použití ------------------- - -Nasazení knihovny Nette DI do aplikace je velmi snadné. Nejprve ji nainstalujeme Composerem (protože stahování zipů je tááák zastaralé): - -```shell -composer require nette/di -``` - -Následující kód vytvoří instanci DI kontejneru podle konfigurace uložené v souboru `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Kontejner se vygeneruje jen jednou, jeho kód se zapíše do cache (adresář `__DIR__ . '/temp'`) a při dalších požadavcích se už jen odsud načítá. - -Pro vytvoření a získání služeb slouží metody `getService()` nebo `getByType()`. Takto vytvoříme objekt `UserController`: - -```php -$database = $container->getByType(UserController::class); -$database->query('...'); -``` - -Během vývoje je užitečné aktivovat auto-refresh mód, kdy se kontejner automaticky přegeneruje, pokud dojde ke změně jakékoliv třídy nebo konfiguračního souboru. Stačí v konstruktoru `ContainerLoader` uvést jako druhý argument `true`. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Použití s frameworkem Nette ---------------------------- - -Jak jsme si ukázali, použití Nette DI není limitované na aplikace psané v Nette Frameworku, můžete jej pomocí pouhých 3 řádků kódu nasadit kdekoliv. -Pokud však vyvíjíte aplikace v Nette Framework, konfiguraci a vytvoření kontejneru má na starosti [Bootstrap|application:bootstrap#toc-konfigurace-di-kontejneru]. diff --git a/dependency-injection/cs/passing-dependencies.texy b/dependency-injection/cs/passing-dependencies.texy deleted file mode 100644 index 50c1718f26..0000000000 --- a/dependency-injection/cs/passing-dependencies.texy +++ /dev/null @@ -1,155 +0,0 @@ -Předávání závislostí -******************** - -<div class=perex> - -Argumenty, nebo v terminologii DI „závislosti“, lze do tříd předávat těmito hlavními způsoby: - -* předávání konstruktorem -* předávání metodou (tzv. setterem) -* nastavením proměnné -* metodou, anotací či atributem *inject* - -</div> - -První tři způsoby platí obecně ve všech objektově orientovaných jazycích, čtvrtý je specifický pro presentery v Nette, takže o něm pojednává [samostatná kapitola |best-practices:inject-method-attribute]. Nyní si jednotlivé možnosti přiblížíme a ukážeme na konkrétních případech. - - -Předávání konstruktorem -======================= - -Závislosti jsou předávány v okamžiku vytváření objektu jako argumenty konstruktoru: - -```php -class MyService -{ - /** @var Cache */ - private $cache; - - public function __construct(Cache $service) - { - $this->cache = $service; - } -} - -$service = new MyService($cache); -``` - -Tato forma je vhodná pro povinné závislosti, které třída nezbytně potřebuje ke své funkci, neboť bez nich nepůjde instanci vytvořit. - -Od PHP 8.0 můžeme použít kratší formu zápisu, která je funkčně ekvivaletní: - -```php -// PHP 8.0 -class MyService -{ - public function __construct( - private Cache $service, - ) { - } -} -``` - -Od PHP 8.1 lze proměnnou označit příznakem `readonly`, který deklaruje, že obsah proměnné se už nezmění: - -```php -// PHP 8.1 -class MyService -{ - public function __construct( - private readonly Cache $service, - ) { - } -} -``` - -DI kontejner předá konstruktoru závislosti automaticky pomocí [autowiringu |autowiring]. Argumenty, které takto předat nelze (např. řetězce, čísla, booleany) [zapíšeme v konfiguraci |services#Argumenty]. - - -Předávání setterem -================== - -Závislosti jsou předávány voláním metody, která je uloží do privátní proměnné. Obvyklou konvencí pojmenování těchto metod je tvar `set*()`, proto se jim říká settery. - -```php -class MyService -{ - /** @var Cache */ - private $cache; - - public function setCache(Cache $service): void - { - $this->cache = $service; - } -} - -$service = new MyService; -$service->setCache($cache); -``` - -Tento způsob je vhodný pro nepovinné závislosti, které nejsou pro funkci třídy nezbytné, neboť není garantováno, že objekt závislost skutečně dostane (tj. že uživatel metodu zavolá). - -Zároveň tento způsob připouští volat setter opakovaně a závislost tak měnit. Pokud to není žádoucí, přidáme do metody kontrolu, nebo od PHP 8.1 označíme proměnnou `$cache` příznakem `readonly`. - -```php -class MyService -{ - /** @var Cache */ - private $cache; - - public function setCache(Cache $service): void - { - if ($this->cache) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $service; - } -} -``` - -Volání setteru definujeme v konfiraci DI kontejneru v [sekci setup |services#Setup]. I tady se využívá automatického předávání závislostí pomocí autowiringu: - -```neon -services: - - - create: MyService - setup: - - setCache -``` - - -Nastavením proměnné -=================== - -Závislosti jsou předávány zapsáním přímo do členské proměnné: - -```php -class MyService -{ - /** @var Cache */ - public $cache; -} - -$service = new MyService; -$service->cache = $cache; -``` - -Tento způsob se považuje za nevhodný, protože členská proměnná musí být deklarována jako `public`. A tudíž nemáme kontrolu nad tím, že předaná závislost bude skutečně daného typu (platilo před PHP 7.4) a přicházíme o možnost reagovat na nově přiřazenou závislost vlastním kódem, například zabránit následné změně. Zároveň se proměnná stává součástí veřejného rozhraní třídy, což nemusí být žádoucí. - -Nastavení proměnné definujeme v konfiraci DI kontejneru v [sekci setup |services#Setup]: - -```neon -services: - - - create: MyService - setup: - - $cache = @\Cache -``` - - -Jaký způsob zvolit? -=================== - -- konstruktor je vhodný pro povinné závislosti, které třída nezbytně potřebuje ke své funkci -- setter je naopak vhodný pro nepovinné závislosti, nebo závislosti, které lze mít možnost dále měnit -- veřejné proměnné vhodné nejsou diff --git a/dependency-injection/cs/services.texy b/dependency-injection/cs/services.texy deleted file mode 100644 index b38ab985fa..0000000000 --- a/dependency-injection/cs/services.texy +++ /dev/null @@ -1,447 +0,0 @@ -Definování služeb -***************** - -.[perex] -Konfigurace je místem, kam umísťujeme definice vlastních služeb. Slouží k tomu sekce `services`. - -Například takto vytvoříme službu pojmenovanou `database`, což bude instance třídy `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Pojmenování služeb slouží k tomu, abychom se na ně mohli [odkazovat|#Odkazování na služby]. Pokud na službu není odkazováno, není ji potřeba pojmenovávat. Místo názvu tak použijeme jen odrážku: - -```neon -services: - - PDO('sqlite::memory:') # anonymní služba -``` - -Jednořádkový zápis lze rozepsat do více řádků a tak umožnit přidání dalších klíčů, jako je například [#setup]. Aliasem pro klíč `create:` je `factory:`. - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Službu poté získáme z DI kontejneru metodou `getService()` podle názvu, nebo ještě lépe metodou `getByType()` podle typu: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Vytvoření služby -================ - -Nejčastěji službu vytváříme prostým vytvořením instance určité třídy: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Což vygeneruje tovární metodu v [DI kontejneru|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret'); -} -``` - -Pro předání [argumentů|#Argumenty] lze alternativně použít i klíč `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Službu může vytvořit také statická metoda: - -```neon -services: - database: My\Database::create(root, secret) -``` - -Odpovídá PHP kódu: - -```php -public function createServiceDatabase(): PDO -{ - return My\Database::create('root', 'secret'); -} -``` - -Předpokládá se, statická metoda `My\Database::create()` má definovanou návratovou hodnotu, kterou DI kontejner potřebuje znát. Pokud ji nemá, zapíšeme typ do konfigurace: - -```neon -services: - database: - create: My\Database::create(root, secret) - type: PDO -``` - -Nette DI nám dává mimořádně silné výrazové prostředky, pomocí kterých můžete zapsat téměř cokoliv. Například se [odkázat|#Odkazování na služby] na jinou službu a zavolat její metodu. Pro jednoduchost se místo `->` používá `::` - -```neon -services: - routerFactory: App\Router\Factory - router: @routerFactory::create() -``` - -Odpovídá PHP kódu: - -```php -public function createServiceRouterFactory(): App\Router\Factory -{ - return new App\Router\Factory; -} - -public function createServiceRouter(): Router -{ - return $this->getService('routerFactory')->create(); -} -``` - -Volání metod lze řetězit za sebe stejně jako v PHP: - -```neon -services: - foo: FooFactory::build()::get() -``` - -Odpovídá PHP kódu: - -```php -public function createServiceFoo() -{ - return FooFactory::build()->get(); -} -``` - - -Argumenty -========= - -Pro předání argumentů lze používat i pojmenované parametry: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' # poziční - username: root # pojmenovaný - password: secret # pojmenovaný - ) -``` - -Při rozepsání argumentů do více řádků je používání čárek volitelné. - -Jako argumenty můžeme samozřejmě použít i [jiné služby|#Odkazování na služby] nebo [parametry|configuration#parametry]: - -```neon -services: - - Foo(@anotherService, %appDir%) -``` - -Odpovídá PHP kódu: - -```php -public function createService01(): Foo -{ - return new Foo($this->getService('anotherService'), '...'); -} -``` - -Pokud se má první argument [autowirovat|autowiring] a chceme přitom uvést argument druhý, vynecháme první znakem `_`, tedy např. `Foo(_, %appDir%)`. Nebo ještě lépe předáme jen druhý argument jako pojmenovaný parametr, např. `Foo(path: %appDir%)`. - -Nette DI a formát NEON nám dává mimořádně silné výrazové prostředky, pomocí kterých můžete zapsat téměř cokoliv. Argumentem tak může být nově vytvořený objekt, lze volat statické metody, metody jiných služeb, nebo pomocí speciálního zápisu i globální funkce: - -```neon -services: - analyser: My\Analyser( - FilesystemIterator(%appDir%) # vytvoření objektu - DateTime::createFromFormat('Y-m-d') # volání statické metody - @anotherService # předání jiné služby - @http.request::getRemoteAddress() # volání metody jiné služby - ::getenv(NETTE_MODE) # volání globální funkce - ) -``` - -Odpovídá PHP kódu: - -```php -public function createServiceAnalyser(): My\Analyser -{ - return new My\Analyser( - new FilesystemIterator('...'), - DateTime::createFromFormat('Y-m-d'), - $this->getService('anotherService'), - $this->getService('http.request')->getRemoteAddress(), - getenv('NETTE_MODE') - ); -} -``` - - -Speciální funkce ----------------- - -V argumentech lze také používat speciální funkce pro přetypování nebo negaci hodnot: - -- `not(%arg%)` negace -- `bool(%arg%)` bezeztrátové přetypování na bool -- `int(%arg%)` bezeztrátové přetypování na int -- `float(%arg%)` bezeztrátové přetypování na float -- `string(%arg%)` bezeztrátové přetypování na string - -```neon -services: - - Foo( - id: int(::getenv('PROJECT_ID')) - productionMode: not(%debugMode%) - ) -``` - -Bezztrátové přetypování se od běžného přetypování v PHP např. pomocí `(int)` liší v tom, že pro nečíselné hodnoty vyhodí výjimku. - -Jako argument lze předávat i více služeb. Pole všech služeb určitého typu (tj. třídy nebo rozhraní) vytvoří funkce `typed()`. Funkce vynechá služby, které mají vypnutý autowiring a lze uvést i více typů oddělených čárkou. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Předávat pole služeb můžete i automaticky pomocí [autowiringu|autowiring#Pole služeb]. - -Pole všech služeb s určitým [tagem|#tagy] vytvoří funkce `tagged()`. Lze uvést i více tagů oddělených čárkou. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Odkazování na služby -==================== - -Na jednotlivé služby se odkazuje pomocí zavináče a názvu služby, takže například `@database`: - -```neon -services: - - create: Foo(@database) - setup: - - setCacheStorage(@cache.storage) -``` - -Odpovídá PHP kódu: - -```php -public function createService01(): Foo -{ - $service = new Foo($this->getService('database')); - $service->setCacheStorage($this->getService('cache.storage')); - return $service; -} -``` - -I na anonymní služby se lze odkazovat přes zavináč, jen místo názvu uvedeme jejich typ (třídu nebo rozhraní). Tohle ovšem obvykle není potřeba dělat díky [autowiringu|autowiring]. - -```neon -services: - - create: Foo(@Nette\Database\Connection) # nebo třeba @\PDO - setup: - - setCacheStorage(@cache.storage) -``` - - -Setup -===== - -V sekci setup uvádíme metody, které se mají zavolat při vytváření služby: - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Odpovídá PHP kódu: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Lze také nastavovat hodnoty proměnných. Podporováno je i přidání prvku do pole, které je potřeba zapsat v uvozovkách, aby nekolidovalo se syntaxí NEON: - - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Odpovídá PHP kódu: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -V setupu lze však volat i statické metody nebo metod jiných služeb. Aktuální službu jim předáme jako `@self`: - - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Odpovídá PHP kódu: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Autowiring -========== - -Pomocí klíče autowired lze službu vyřadit z autowiringu nebo jeho chování ovlivnit. Více v [kapitole o autowiringu|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # služba foo je vyřazena z autowiringu -``` - - -Tagy -==== - -Jednotlivým službám lze přidávat uživatelské informace v podobě tzv. tagů: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Tagy mohou mít i hodnotu: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Pole služeb s určitými tagy lze předat jako argument pomocí funkce `tagged()`. Lze uvést i více tagů oddělených čárkou. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -Názvy služeb lze získat z DI kontejneru metodou `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names je pole obsahující název služby a hodnotu tagu -// např. ['foo' => 'monolog.logger.event', ...] -``` - - -Režim Inject -============ - -Pomocí příznaku `inject: true` se aktivuje předávání závislostí přes veřejné proměnné s anotací [inject |best-practices:inject-method-attribute#Anotace inject] a metody [inject*() |best-practices:inject-method-attribute#metody inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -V základním nastavení je `inject` aktivováno pouze pro presentery. - - -Modifikace služeb -================= - -V DI kontejneru je řada služeb, které přidaly vestavěné nebo [vaše rozšíření|#rozšíření]. Definice těchto služeb lze v konfiguraci pozměnit. Třeba u služby `application.application`, což je standardně objekt `Nette\Application\Application`, můžeme změnit třídu: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Příznak `alteration` je informativní a říká, že jen modifikujeme existující službu. - -Můžeme také doplnit setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Při přepisování služby můžeme chtít odstranit původní argumenty, položky setup nebo tagy, k čemuž slouží `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Službu přidanou rozšířením lze také z kontejneru odstranit: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/en/@home.texy b/dependency-injection/en/@home.texy deleted file mode 100644 index 99c96616bb..0000000000 --- a/dependency-injection/en/@home.texy +++ /dev/null @@ -1,22 +0,0 @@ -Dependency Injection -******************** - -.[perex] -Dependency Injection is a design pattern that will fundamentally change the way you look at code and development. It opens the way to a world of cleanly designed and sustainable applications. - -- [What is Dependency Injection? |introduction] -- [What is DI Container? |container] -- [Passing Dependencies |passing-dependencies] - - -Nette DI --------- - -The `nette/di` package provides an extremely advanced compiled DI container for PHP. - -- [Nette DI Container |nette-container] -- [Configuration |configuration] -- [Service Definitions |services] -- [Autowiring |autowiring] -- [Generated Factories |factory] -- [Creating Extensions for Nette DI|extensions] diff --git a/dependency-injection/en/@left-menu.texy b/dependency-injection/en/@left-menu.texy deleted file mode 100644 index 30092630f6..0000000000 --- a/dependency-injection/en/@left-menu.texy +++ /dev/null @@ -1,15 +0,0 @@ -Dependency Injection -******************** -- [What is DI? |introduction] -- [What is DI Container? |container] -- [Passing Dependencies |passing-dependencies] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Configuration |configuration] -- [Service Definitions |services] -- [Autowiring |autowiring] -- [Generated Factories |factory] -- [Creating Extensions for Nette DI|extensions] diff --git a/dependency-injection/en/autowiring.texy b/dependency-injection/en/autowiring.texy deleted file mode 100644 index 92c827f3de..0000000000 --- a/dependency-injection/en/autowiring.texy +++ /dev/null @@ -1,260 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring is a great feature that can automatically pass services to the constructor and other methods, so we do not need to write them at all. It saves you a lot of time. - -This allows us to skip the vast majority of arguments when writing service definitions. Instead of: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Just write: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring is driven by types, so `ArticleRepository` class must be defined as follows: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -To use autowiring, there must be **just one service** for each type in the container. If there were more, autowiring would not know which one to pass and throw away an exception: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # THROWS EXCEPTION, both mainDb and tempDb matches -``` - -The solution would be to either bypass autowiring and explicitly state the service name (i.e. `articles: Model\ArticleRepository(@mainDb)`). However, it is more convenient to [disable|#Disabled autowiring] autowiring of one services, or the first service [prefer|#Preferred Autowiring]. - - -Disabled Autowiring -------------------- - -You can disable service autowiring by using the `autowired: no` option: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # removes tempDb from autowiring - - articles: Model\ArticleRepository # therefore passes mainDb to constructor -``` - -The `articles` service does not throw the exception that there are two matching services of type `PDO` (i.e. `mainDb` and `tempDb`) that can be passed to the constructor, because it only sees the `mainDb` service. - -.[note] -Configuring autowiring in Nette works differently than in Symfony, where the `autowire: false` option says that autowiring should not be used for service constructor arguments. -In Nette, autowiring is always used, whether for arguments of the constructor or any other method. The `autowired: false` option says that the service instance should not be passed anywhere using autowiring. - - -Preferred Autowiring --------------------- - -If we have more services of the same type and one of them has the `autowired` option, this service becomes the preferred one: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # makes it preferred - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -The `articles` service does not throw the exception that there are two matching `PDO` services (i.e. `mainDb` and `tempDb`), but uses the preferred service, i.e. `mainDb`. - - -Collection of Services ----------------------- - -Autowiring can also pass an array of services of a particular type. Since PHP cannot natively notate the type of array items, in addition to the `array` type, a phpDoc comment with the item type like `ClassName[]` must be added: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -The DI container then automatically passes an array of services matching the given type. It will omit services that have autowiring turned off. - -If you can't control the form of the phpDoc comment, you can pass an array of services directly in the configuration using [`typed()`|services#Special Functions]. - - -Scalar Arguments ----------------- - -Autowiring can only pass objects and arrays of objects. Scalar arguments (e.g. strings, numbers, booleans) [write in configuration |services#Arguments]. -An alternative is to create a [settings-object |best-practices:passing-settings-to-presenters] that encapsulates a scalar value (or multiple values) as an object, which can then be passed again using autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly can be used since PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -You create a service by adding it to the configuration: - -```neon -services: - - MySettings('any value') -``` - -All classes will then request it via autowiring. - - -Narrowing of Autowiring ------------------------ - -For individual services, autowiring can be narrowed to specific classes or interfaces. - -Normally, autowiring passes the service to each method parameter whose type the service corresponds to. Narrowing means that we specify conditions that the types specified for the method parameters must satisfy for the service to be passed to them. - -Let's take an example: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -If we registered them all as services, autowiring would fail: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # THROWS EXCEPTION, both parent and child matches - childDep: ChildDependent # passes the service 'child' to the constructor -``` - -The `parentDep` service throws the exception `Multiple services of type ParentClass found: parent, child` because both `parent` and `child` fit into its constructor and autowiring can not make a decision on which one to choose. - -For service `child`, we can therefore narrow down its autowiring to `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # alternative: 'autowired: self' - - parentDep: ParentDependent # THROWS EXCEPTION, the 'child' can not be autowired - childDep: ChildDependent # passes the service 'child' to the constructor -``` - -The `parentDep` service is now passed to the `parentDep` service constructor, since it is now the only matching object. The `child` service is no longer passed in by autowiring. Yes, the `child` service is still of type `ParentClass`, but the narrowing condition given for the parameter type no longer applies, i.e. it is no longer true that `ParentClass` *is a supertype* of `ChildClass`. - -In the case of `child`, `autowired: ChildClass` could be written as `autowired: self` as the `self` means current service type. - -The `autowired` key can include several classes and interfaces as array: - -```neon -autowired: [BarClass, FooInterface] -``` - -Let's try to add interfaces to the example: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -When we do not limit the `child` service, it will fit into the constructors of all `FooDependent`, `BarDependent`, `ParentDependent` and `ChildDependent` classes and autowiring will pass it there. - -However, if we narrow its autowiring to `ChildClass` using `autowired: ChildClass` (or `self`), autowiring it only passes it to the `ChildDependent` constructor, because it requires an argument of type `ChildClass` and `ChildClass` *is of type* `ChildClass`. No other type specified for the other parameters is a superset of `ChildClass`, so the service is not passed. - -If we restrict it to `ParentClass` using `autowired: ParentClass`, autowiring will pass it again to the `ChildDependent` constructor (since the required type `ChildClass` is a superset of `ParentClass`) and to the `ParentDependent` constructor too, since the required type of `ParentClass` is also matching. - -If we restrict it to `FooInterface`, it will still autowire to `ParentDependent` (the required type `ParentClass` is a supertype of `FooInterface`) and `ChildDependent`, but additionally to the `FooDependent` constructor, but not to `BarDependent`, since `BarInterface` is not a supertype of `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # passes the service child to the constructor - barDep: BarDependent # THROWS EXCEPTION, no service would pass - parentDep: ParentDependent # passes the service child to the constructor - childDep: ChildDependent # passes the service child to the constructor -``` diff --git a/dependency-injection/en/configuration.texy b/dependency-injection/en/configuration.texy deleted file mode 100644 index a43f9a653c..0000000000 --- a/dependency-injection/en/configuration.texy +++ /dev/null @@ -1,318 +0,0 @@ -Configuring DI Container -************************ - -.[perex] -Overview of configuration options for the Nette DI container. - - -Configuration File -================== - -Nette DI container is easy to control using configuration files. They are usually written in [NEON format|neon:format]. We recommend to use [editors with support|best-practices:editors-and-tools#ide-editor] for this format for editing. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI Container .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensions]: "Install additional DI extensions .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Including files]: "Including files .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parameters]: "Parameters .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Automatic service registration .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Services .[prism-token prism-comment]" -</pre> - -To write a string containing the character `%`, you must escape it by doubling it to `%%`. .[note] - - -Parameters -========== - -You can define parameters that can then be used as part of service definitions. This can help to separate out values that you will want to change more regularly. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -You can refer to `foo` parameter via `%foo%` elsewhere in any config file. They can also be used inside strings like `'%wwwDir%/images'`. - -Parameters do not need to be just strings, they can also be array values: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -You can refer to single key as `%mailer.user%`. - -If you need to get the value of any parameter in your code, for example in your class, then pass it to this class. For example, in the constructor. There is no global configuration object which can classes query for parameter values. This would be against to the principle of dependency injection. - - -Services -======== - -See [separate chapter|services]. - - -Decorator -========= - -How to bulk edit all services of a certain type? Need to call a certain method for all presenters inheriting from a particular common ancestor? That's where the decorator comes from. - -```neon -decorator: - # for all services that are instances of this class or interface - App\Presenters\BasePresenter: - setup: - - setProjectId(10) # call this method - - $absoluteUrls = true # and set the variable -``` - -Decorator can also be used to set [tags|services#Tags] or turn on [inject mode|services#Inject Mode]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Technical settings of the DI container. - -```neon -di: - # shows DIC in Tracy Bar? - debugger: ... # (bool) defaults to true - - # parameter types that you never autowire - excluded: ... # (string[]) - - # the class from which the DI container inherits - parentClass: ... # (string) defaults to Nette\DI\Container -``` - - -Metadata Export ---------------- - -The DI container class also contains a lot of metadata. You can reduce it by reducing the metadata export. - -```neon -di: - export: - # to export parameters? - parameters: false # (bool) defaults to true - - # export tags and which ones? - tags: # (string[]|bool) the default is all - - event.subscriber - - # export data for autowiring and which? - types: # (string[]|bool) the default is all - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -If you don't use the `$container->parameters` array, you can disable parameter export. Furthermore, you can export only those tags through which you get services using the `$container->findByTag(...)` method. -If you don't call the method at all, you can completely disable tag export with `false`. - -You can significantly reduce the metadata for [autowiring] by specifying the classes you use as a parameter to the `$container->getByType()` method. -And again, if you don't call the method at all (or only in [application:bootstrap] to get `Nette\Application\Application`), you can disable the export entirely with `false`. - - -Extensions -========== - -Registration of other DI extensions. In this way we add, for example, DI extension `Dibi\Bridges\Nette\DibiExtension22` under the name `dibi`: - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Then we configure it in it's section called also `dibi`: - -```neon -dibi: - host: localhost -``` - -You can also add a extension class with parameters: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Including Files -=============== - -Additional configuration files can be inserted in the `includes` section: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -The name `parameters.php` is not a typo, the configuration can also be written in a PHP file, which returns it as an array: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -If items with the same keys appear within configuration files, they will be [overwritten or merged |#Merging] in the case of arrays. Later included file has a higher priority than the previous one. The file in which the `includes` section is listed has a higher priority than the files included in it. - - -Search -====== - -The automatic adding of services to the DI container makes work extremely pleasant. Nette automatically adds presenters to the container, but you can easily add any other classes. - -Just specify in which directories (and subdirectories) the classes should be search for: - -```neon -search: - # you choose the section names yourself - myForms: - in: %appDir%/Forms - - model: - in: %appDir%/Model -``` - -Usually, however, we don't want to add all the classes and interfaces, so we can filter them: - -```neon -search: - myForms: - in: %appDir%/Forms - - # filtering by file name (string|string[]) - files: - - *Factory.php - - # filtering by class name (string|string[]) - classes: - - *Factory -``` - -Or we can select classes that inherit or implement at least one of the following classes: - - -```neon -search: - myForms: - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -You can also define negative rules, ie class name masks or ancestors and if they comply, the service will not be added to the DI container: - -```neon -search: - myForms: - exclude: - classes: ... - extends: ... - implements: ... -``` - -Tags can be set for added services: - -```neon -search: - myForms: - tags: ... -``` - - -Merging -======= - -If items with the same keys appear in more configuration files, they will be overwritten or merged in the case of arrays. The later included file has a higher priority. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>result</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -To prevent merging of a certain array use exclamation mark right after the name of the array: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>result</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - - -{{maintitle: Dependency Injection Configuration}} diff --git a/dependency-injection/en/container.texy b/dependency-injection/en/container.texy deleted file mode 100644 index 975d8bbf7f..0000000000 --- a/dependency-injection/en/container.texy +++ /dev/null @@ -1,145 +0,0 @@ -What Is DI Container? -********************* - -.[perex] -Dependency injection container (DIC) is a class that can instantiate and configure objects. - -It may surprise you, but in many cases you don't need a dependency injection container to take advantage of dependency injection (DI for short). After all, even in [previous chapter|introduction] we showed specific examples of DI and no container was needed. - -However, if you need to manage a large number of different objects with many dependencies, a dependency injection container will be really useful. Which is perhaps the case for web applications built on a framework. - -In the previous chapter, we introduced the classes `Article` and `UserController`. Both of them have some dependencies, namely database and factory `ArticleFactory`. And for these classes, we will now create a container. Of course, for such a simple example, it doesn't make sense to have a container. But we'll create one to show how it looks and works. - -Here is a simple hardcoded container for the above example: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createUserController(): UserController - { - return new UserController($this->createArticleFactory()); - } -} -``` - -The usage would look like this: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -We just ask the container for the object and no longer need to know anything about how to create it or what its dependencies are; the container knows all that. The dependencies are injected automatically by the container. That's its power. - -So far, the container has everything hardcoded. So we take the next step and add parameters to make the container really useful: - -```php -class Container -{ - private array $parameters; - - public function __construct(array $parameters) - { - $this->parameters = $parameters; - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'] - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Astute readers may have noticed a problem. Every time I get an object `UserController`, a new instance `ArticleFactory` and database is also created. We definitely don't want that. - -So we add a method `getService()` that will return the same instances over and over again: - -```php -class Container -{ - private array $parameters; - private array $services = []; - - public function __construct(array $parameters) - { - $this->parameters = $parameters; - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') calls createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -The first call to e.g. `$container->getService('Database')` will have `createDatabase()` create a database object, which it will store in the array `$services` and return it directly on the next call. - -We also modify the rest of the container to use `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -By the way, the term service refers to any object managed by the container. Hence the method name `getService()`. - -Done. We have a fully functional DI container! And we can use it: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -As you can see, it's not difficult to write a DIC. It's notable that the objects themselves don't know that a container is creating them. Thus, it is possible to create any object in PHP this way without affecting their source code. - -Manually creating and maintaining a container class can become a nightmare rather quickly. Therefore, in the next chapter we will talk about [Nette DI Container|nette-container], which can generate and update itself almost automatically. - - -{{maintitle: What is Dependency Injection Container?}} diff --git a/dependency-injection/en/extensions.texy b/dependency-injection/en/extensions.texy deleted file mode 100644 index 4e016ce8b7..0000000000 --- a/dependency-injection/en/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Creating Extensions for Nette DI -******************************** - -.[perex] -Generating an DI container in addition to configuration files also affect the so-called *extensions*. We activate them in the configuration file in the `extensions` section. - -This is how we add the extension represented by class `BlogExtension` with name `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Each compiler extension inherits from [api:Nette\DI\CompilerExtension] and can implement following methods that are called during DI compilation: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -This method is called first. It defines schema used to validate configuration parameters. - -Extensions are configured in a section whose name is the same as the one under which the extension was added, eg `blog`. - -```neon -# same name as my extension -blog: - postsPerPage: 10 - comments: false -``` - -We will define a schema describing all configuration options, including their types, accepted values and possibly default values: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), - ]); - } -} -``` - -See the [Schema |schema:] for documentation. Additionally, you can specify which options can be [dynamic |application:bootstrap#Dynamic Parameters] using `dynamic()`, for example `Expect::int()->dynamic()`. - -We access configuration through the `$this->config`, which is an object `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -This method is used to add services to the container. This is done by [api:Nette\DI\ContainerBuilder]: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // or setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -The convention is to prefix the services added by an extension with its name so that no name conflicts arise. This is done by `prefix()`, so if the extension is called 'blog', the service will be called `blog.articles`. - -If we need to rename a service, we can create an alias with its original name to maintain backward compatibility. Similarly this is what Nette does for eg `routing.router`, which is also available under the earlier name `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Retrieve Services from a File ------------------------------ - -We can create services using the ContainerBuilder API, but also we can add them via the familiar NEON configuration file and its `services` section. The prefix `@extension` represents the current extension. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -We will add services this way: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // load the configuration file for the extension - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -The method is called when the container contains all the services added by individual extensions in `loadConfiguration` methods as well as user configuration files. At this phase of assembling, we can then modify service definitions or add links between them. You can use the `findByTag()` method to search for services by tags, or `findByType()` method to search by class or interface. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } - } -} -``` - - -afterCompile() .[method] -======================== - -At this phase, the container class is already generated as a [ClassType |php-generator:#classes] object, it contains all the methods that the service creates, and is ready for caching as PHP file. We can still edit the resulting class code at this point. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[wiki-method] -============================== - -The Configurator calls the initialization code after [container creation |application:bootstrap#index.php], which is created by writing to an object `$this->initialization` using [method addBody() |php-generator:#method-and-function-body]. - -We will show an example of how to start a session or start services that have the `run` tag using initialization code: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // automatic session startup - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // services with tag 'run' must be created after the container is instantiated - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/en/factory.texy b/dependency-injection/en/factory.texy deleted file mode 100644 index 4e882beb9f..0000000000 --- a/dependency-injection/en/factory.texy +++ /dev/null @@ -1,228 +0,0 @@ -Generated Factories -******************* - -.[perex] -Nette DI can automatically generate factory code based on the interface, which saves you from writing code. - -A factory is a class that creates and configures objects. It therefore passes their dependencies to them as well. We showed what such a factory looks like in [introduction|introduction#factory]: - -```php -class ArticleFactory -{ - private Nette\Database\Connection $db; - - public function __construct(Nette\Database\Connection $db) - { - $this->db = $db; - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI can generate factory code automatically. All you have to do is create an interface and Nette DI will generate an implementation. The interface must have exactly one method named `create` and declare a return type: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -So the factory `ArticleFactory` has a method `create` that creates objects `Article`. Class `Article` might look like the following, for example: - -```php -class Article -{ - private $db; - - public function __construct(Nette\Database\Connection $db) - { - $this->db = $db; - } -} -``` - -Add the factory to the configuration file: - -```neon -services: - - ArticleFactory -``` - -Nette DI will generate the corresponding factory implementation. - -Thus, in the code that uses the factory, we request the object by interface and Nette DI uses the generated implementation: - -```php -class UserController -{ - private $articleFactory; - - public function __construct(ArticleFactory $articleFactory) - { - $this->articleFactory = $articleFactory; - } - - public function foo() - { - // let the factory create an object - $article = $this->articleFactory->create(); - } -} -``` - - -Parameterized Factory -===================== - -The factory method `create` can accept parameters which it then passes to the constructor. For example, let's add an article author ID to the class `Article`: - -```php -class Article -{ - private $db; - private $authorId; - - public function __construct(Nette\Database\Connection $db, int $authorId) - { - $this->db = $db; - $this->authorId = $authorId; - } -} -``` - -We will also add the parameter to the factory: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Because the parameter in the constructor and the parameter in the factory have the same name, Nette DI will pass them automatically. - - -Advanced Definition -=================== - -The definition can also be written in multi-line form using the key `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -When writing in this longer way, it is possible to provide additional arguments for the constructor in the key `arguments` and additional configuration using `setup`, just as for normal services. - -Example: if the method `create()` did not accept the parameter `$authorId`, we could specify a fixed value in the configuration that would be passed to the constructor `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Or, conversely, if `create()` did accept the parameter `$authorId` but it was not part of the constructor and was passed by method `Article::setAuthorId()`, we would refer to it in section `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Besides factories, Nette can also generate so called accessors. Accessor is an object with `get()` method returning a particular service from the DI container. Multiple `get()` calls will always return the same instance. - -Accessors bring lazy-loading to dependencies. Let's have a class logging errors to a special database. If the database connection would be passed as a dependency in its constructor, the connection would need to be always created although it would be used only rarely when an error appears so the connection would stay mostly unused. -Instead, the class can pass an accessor and when its `get()` method is called, only then the database object is created: - -How to create an accessor? Write an interface only and Nette DI will generate the implementation. The interface must have exactly one method called `get` and must declare the return type: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Add the accessor to the configuration file together with the definition of the service the accessor will return: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -The accessor returns a service of type `PDO` and because there's only one such service in the configuration, the accessor will return it. With multiple configured services of that type you can specify which one should be returned using its name, for example `- PDOAccessor(@db1)`. - - -Multifactory/Accessor -===================== -So far, the factories and accessors could only create or return just one object. A multifactory combined with an accessor can be created as well. The interface of such multifactory class can consist of multiple methods called `create<name>()` and `get<name>()`, for example: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function createFoo(): Model\Foo; - function getDb(): PDO; -} -``` - -Instead of passing multiple generated factories and accessors, you can pass just one complex multifactory. - -Alternatively, you can use `create()` and `get()` with a parameter instead of multiple methods: - -```php -interface MultiFactoryAlt -{ - function create($name); - function get($name): PDO; -} -``` - -In this case, `MultiFactory::createArticle()` does the same thing as `MultiFactoryAlt::create('article')`. However, the alternative syntax has a few disadvantages. It's not clear which `$name` values are supported and the return type cannot be specified in the interface when using multiple different `$name` values. - - -Definition with a List ----------------------- -Hos to define a multifactory in your configuration? Let's create three services which will be returned by the multifactory, and the multifactory itself: - -```neon -services: - article: Article - - Model\Foo - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # createArticle() - foo: @Model\Foo # createFoo() - db: @\PDO # getDb() - ) -``` - - -Definition with Tags --------------------- - -Another option how to define a multifactory is to use [tags|services#Tags]: - -```neon -services: - - App\Router\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/en/introduction.texy b/dependency-injection/en/introduction.texy deleted file mode 100644 index e6d1102b6d..0000000000 --- a/dependency-injection/en/introduction.texy +++ /dev/null @@ -1,470 +0,0 @@ -What Is Dependency Injection? -***************************** - -.[perex] -This chapter introduces you to the basic programming practices that underpin the entire Nette framework and that you should follow when writing your own applications. These are the basics needed to write clean, understandable, and maintainable code. - -If you learn and follow these rules, the framework will be there for you every step of the way. It will handle routine tasks for you and make you as comfortable as possible so you can focus on the logic itself. - -The principles we will show here are quite simple. You have nothing to worry about. - - -Remember Your First Program? ----------------------------- - -We have no idea what language you wrote it in, but if it were PHP, it would probably look something like this: - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} - -echo addition(23, 1); // prints 24 -``` - -A few trivial lines of code, yet so many key concepts are hidden in them. We see that there are variables. That code is broken down into smaller units, which are functions, for example. That we pass them input arguments and they return results. All that's missing are conditions and loops. - -The fact that we pass input to a function and it returns a result is a perfectly understandable concept that is used in other fields, such as mathematics. - -A function has a signature, which consists of its name, a list of parameters and their types, and finally the type of return value. As users, we are interested in the signature; we usually don't need to know anything about the internal implementation. - -Now imagine that the signature of a function looks like this: - -```php -function addition(float $x): float -``` - -An addition with one parameter? That's weird... How about this? - -```php -function addition(): float -``` - -That's really weird, isn't it? How do you think the function is used? - -```php -echo addition(); // what does it prints? -``` - -Looking at such code, we are confused. Not only a beginner would not understand it, even a skilled programmer would not understand such code. - -Wondering what such a function would actually look like inside? Where would it get the addends? It would probably get them *somehow* on its own, like this: - -```php -function addition(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -It turns out that there are hidden bindings to other functions (or static methods) in the body of the function, and to find out where the addends actually come from, we have to dig further. - - -Not This Way! -------------- - -The design we have just shown is the essence of many negative features: - -- the function signature pretended that it didn't need addends, which confused us -- we have no idea how to make the function calculate with two other numbers -- we had to look into the code to see where it takes the addends -- we discovered hidden bindings -- to fully understand, we need to explore these bindings as well - -And is it even the job of the addition function to procure inputs? Of course it isn't. Its responsibility is only to add. - - -We don't want to encounter such code, and we certainly don't want to write it. The remedy is simple: go back to basics and just use parameters: - - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Rule #1: Use Parameters ------------------------ - -The most important rule is: **all data that functions or classes need must be passed to them**. - -If we break this rule, it will be impossible to make the code understandable, clean and sustainable. - -If we follow it, we're on our way to code without hidden constraints. Towards code that is understandable not only to the author, but to anyone who reads it afterwards. Where everything is understandable from the signatures of functions and classes and there is no need to search for hidden secrets in the implementation. - -This technique of passing arguments is technically called **dependency injection**. - -(Don't confuse dependency injection with a "dependency injection container"; it is something radically different, and we'll cover containers in [next chapter|container].) - - -From Functions to Classes -------------------------- - -And how do classes relate to this? A class is a more complex entity than a simple function, but rule #1 applies here as well. There are just [more ways to pass arguments|passing-dependencies]. For example, quite similar to the case of a function: - -```php -class Math -{ - public function addition(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Math; -echo $math->addition(23, 1); // 24 -``` - -Or by using other methods, or the constructor directly: - -```php -class Addition -{ - private float $a; - private float $b; - - public function __construct(float $a, float $b) - { - $this->a = $a; - $this->b = $b; - } - - public function calculate(): float - { - return $this->a + $this->b; - } - -} - -$addition = new Addition(23, 1); -echo $addition->calculate(); // 24 -``` - -Both examples are completely in compliance with dependency injection. - - -Real-Life Examples ------------------- - -In the real world, you won't write classes to add numbers. Let's move on to real-life examples. - -Let's have a class `Article` representing a blog article: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // save the article to the database - } -} -``` - -and the usage will be as follows: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -Method `save()` will store the article in a database table. Implementing it using [Nette Database |database:] would be a piece of cake, if it weren't for one hitch: where does `Article` get the database connection, i.e. the class object `Nette\Database\Connection`? - -It seems we have plenty of options. It can take it from some static variable. Or inherit from the class that will provide the database connection. Or take advantage of a so-called singleton. Or the so-called facades that are used in Laravel: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Great, we've solved the problem. - -Or have we? - -Recall [#rule #1: use parameters]: we have to pass all the data the class needs to them. Because if we don't, and we break the rule, we've started down the path to dirty code full of hidden bindings, incomprehensibility, and the result will be an application that's a pain to maintain and develop. - -The user of class `Article` has no idea where method `save()` stores the article. In a database table? In which one, production or development? And how can this be changed? - -The user has to look at how the method `save()` is implemented to find the use of the method `DB::insert()`. So he has to search further to find out how this method procures a database connection. And hidden bindings can form quite a long chain. - -Hidden bindings, Laravel facades, or static variables are never present in clean, well-designed code. In clean and well-designed code, arguments are passed: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Even more practical, as we'll see next, is to use a constructor: - -```php -class Article -{ - private Nette\Database\Connection $db; - - public function __construct(Nette\Database\Connection $db) - { - $this->db = $db; - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -If you are going to write a class that requires a database, for example, don't figure out where to get it from, but let it be passed to you. Perhaps as a parameter to a constructor or other method. Declare dependencies. Expose them in the API of your class. You'll get understandable and predictable code. - -How about this class that logs error messages: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -What do you think, did we follow [#rule #1: use parameters]? - -We didn't. - -The class *obtains* the key information, the directory containing the log file, from a constant. - -See an example usage: - -```php -$logger = new Logger; -$logger->log('The temperature is 23 °C'); -$logger->log('The temperature is 10 °C'); -``` - -Without knowing the implementation, could you answer the question where the messages are written? Would it suggest to you that the existence of the LOG_DIR constant is necessary for it to work? And would you be able to create a second instance that writes to a different location? Certainly not. - -Let's fix the class: - -```php -class Logger -{ - private string $file; - - public function __construct(string $file) - { - $this->file = $file; - } - - public function log(string $message) - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -The class is now much clearer, more configurable and therefore more useful. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('The temperature is 15 °C'); -``` - - -But I Don’t Care! ------------------ - -*"When I create an Article object and call save(), I don't want to deal with the database, I just want it to be saved to the one I have set in the configuration. "* - -*"When I use Logger, I just want the message to be written, and I don't want to deal with where. Let the global settings be used. "* - -These are correct comments. - -As an example, let's take a class that sends out newsletters and logs how that went: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Emails have been sent out'); - - } catch (Exception $e) { - $logger->log('An error occurred during the sending'); - throw $e; - } - } -} -``` - -However, the new `Logger`, which no longer uses the `LOG_DIR` constant, requires the path to the file in the constructor. How to solve this? The `NewsletterDistributor` class doesn't care where the messages are written, it just wants to write them. - -The solution is again [#rule #1: use parameters]: pass all the data the class needs to it. - -So we pass the path to the log to the constructor, which we then use when creating the `Logger` object? No. Because the path is not the data that the `NewsletterDistributor` class needs; that's what `Logger` needs. The class needs the logger itself. And we're going to pass that: - - -```php -class NewsletterDistributor -{ - private Logger $logger; - - public function __construct(Logger $logger) - { - $this->logger = $logger; - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Emails have been sent out'); - - } catch (Exception $e) { - $this->logger->log('An error occurred during the sending'); - throw $e; - } - } -} -``` - -Now it is clear from the signatures of class `NewsletterDistributor` that logging is part of its functionality. And you have the option to replace the logger with another one. - -While in the whole application we can be happy with a single instance of the logger and pass it wherever something is logged, it is different in the case of the `Article` class. We will want to create multiple instances of it. How to deal with the database dependency in the constructor? As an example, let's take a controller that is supposed to save an article to the database after submitting a form: - -```php -class UserController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -A possible solution is suggested: pass the database object to the `UserController` by the constructor and use `$article = new Article($this->db)`. - -As in the previous case, this is not the correct practice. The database is not a `UserController` dependency, but an `Article` dependency. Moreover, the moment the constructor of the `Article` class is somehow changed (a new parameter is added), we will have to modify the code in all the places where instances are created. - -The solution is factories. - - -Rule #2: Use Factories ----------------------- - -By removing the hidden bindings and passing all data as arguments, we get more configurable and flexible classes. Therefore, we still need something to create and configure those more flexible classes. We'll call it a factory. - -The rule of thumb is: if a class has dependencies, leave the creation of their instances to the factory. - -Factories are a smarter replacement for the `new` operator in the dependency injection world. - - -Factory -------- - -A factory is a class that creates and configures objects. The factory that produces `Article` will be called `ArticleFactory` and its use in the controller will be as follows: - -```php -class UserController extends Controller -{ - private ArticleFactory $articleFactory; - - public function __construct(ArticleFactory $articleFactory) - { - $this->articleFactory = $articleFactory; - } - - public function formSubmitted($data) - { - // let the factory create an object - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -A factory implementation might look like this: - - -```php -class ArticleFactory -{ - private Nette\Database\Connection $db; - - public function __construct(Nette\Database\Connection $db) - { - $this->db = $db; - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -At this point, when the signature of the class constructor `Article` changes, the only part of the code that needs to react to this is the factory `ArticleFactory`. Any other code that works with `Article` objects, such as `UserController`, is unaffected. - -You may be tapping your forehead right now wondering how we actually helped ourselves. The amount of code has grown and moved from the controller to a separate class. However, Nette DI has a hidden ace up its sleeve. It understands the concept of factories and can even [write such a service for us|factory]. So instead of the `ArticleFactory` class, we could just create an interface: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -But we're getting a little ahead of that now, we'll get to that in a minute. - - -Summary -------- - -At the beginning of this chapter, we promised to demonstrate a simple principle of how to design applications. Although the principle itself is simple (give the classes the data they need), what follows from it requires more thought. Feel free to read this chapter several times. - -Programmers who have thrown out old habits and started using dependency injection consistently consider this a pivotal moment in their professional lives. It opened up a world of clear and sustainable applications. - -Now we will see what [Dependency Injection Container|container] is. diff --git a/dependency-injection/en/nette-container.texy b/dependency-injection/en/nette-container.texy deleted file mode 100644 index 89331d5a46..0000000000 --- a/dependency-injection/en/nette-container.texy +++ /dev/null @@ -1,82 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI is one of the most interesting Nette libraries. It can generate and automatically update compiled DI containers that are extremely fast and amazingly easy to configure. - -The services to be created by a DI container are usually defined using configuration files in [NEON format|neon:format]. The container we manually created in [previous section|container] would be written as follows: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -The notation is really brief. - -All dependencies declared in the constructors of the `ArticleFactory` and `UserController` classes are found and passed by Nette DI itself thanks to the so-called [autowiring], so there is no need to specify anything in the configuration file. -So even if the parameters change, you don't need to change anything in the configuration. Nette will automatically regenerate the container. You can concentrate there purely on application development. - -If you want to pass dependencies using setters, use the [setup|services#setup] section to do so. - -Nette DI will directly generate the PHP code for the container. The result is thus a `.php` file that you can open and study. This allows you to see exactly how the container works. You can also debug it in the IDE and step through it. And most importantly: the generated PHP is extremely fast. - -Nette DI can also generate [factory] code based on the supplied interface. Therefore, instead of the `ArticleFactory` class, we only need to create an interface in the application: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -You can find the full example [on GitHub|https://github.com/nette-examples/di-example-doc]. - - -Standalone Use --------------- - -Utilization the Nette DI library in an application is very easy. First we install it with Composer (because downloading zip files is so outdated): - -```shell -composer require nette/di -``` - -The following code creates an instance of the DI container according to the configuration stored in the `config.neon` file: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -The container is generated only once, its code is written to the cache (the `__DIR__ . '/temp'` directory) and on subsequent requests it is only read from there. - -The `getService()` or `getByType()` methods are used to create and retrieve services. This is how we create the `UserController` object: - -```php -$database = $container->getByType(UserController::class); -$database->query('...'); -``` - -During development, it is useful to enable auto-refresh mode, where the container is automatically regenerated if any class or configuration file is changed. Just provide `true` as the second argument in the `ContainerLoader` constructor. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Using It with the Nette Framework ---------------------------------- - -As we have shown, the use of Nette DI is not limited to applications written in the Nette Framework, you can deploy it anywhere with just 3 lines of code. -However, if you are developing applications in the Nette Framework, the configuration and creation of the container is handled by [Bootstrap|application:bootstrap#toc-di-container-configuration]. diff --git a/dependency-injection/en/passing-dependencies.texy b/dependency-injection/en/passing-dependencies.texy deleted file mode 100644 index 4e0a83acd2..0000000000 --- a/dependency-injection/en/passing-dependencies.texy +++ /dev/null @@ -1,155 +0,0 @@ -Passing Dependencies -******************** - -<div class=perex> - -Arguments, or "dependencies" in DI terminology, can be passed to classes in the following main ways: - -* passing by constructor -* passing by method (called a setter) -* by setting a property -* by method, annotation or attribute *inject* - -</div> - -The first three methods apply in general in all object-oriented languages, the fourth is specific to Nette presenters, so it is discussed in [separate chapter |best-practices:inject-method-attribute]. We will now take a closer look at each of these options and show them with specific examples. - - -Constructor Injection -===================== - -Dependencies are passed as arguments to the constructor when the object is created: - -```php -class MyService -{ - /** @var Cache */ - private $cache; - - public function __construct(Cache $service) - { - $this->cache = $service; - } -} - -$service = new MyService($cache); -``` - -This form is useful for mandatory dependencies that the class absolutely needs to function, as without them the instance cannot be created. - -Since PHP 8.0, we can use a shorter form of notation that is functionally equivalent: - -```php -// PHP 8.0 -class MyService -{ - public function __construct( - private Cache $service, - ) { - } -} -``` - -As of PHP 8.1, a property can be marked with a flag `readonly` that declares that the contents of the property will not change: - -```php -// PHP 8.1 -class MyService -{ - public function __construct( - private readonly Cache $service, - ) { - } -} -``` - -DI container passes dependencies to the constructor automatically using [autowiring]. Arguments that cannot be passed in this way (e.g. strings, numbers, booleans) [write in configuration |services#Arguments]. - - -Setter Injection -================ - -Dependencies are passed by calling a method that stores them in a private properties. The usual naming convention for these methods is of the form `set*()`, which is why they are called setters. - -```php -class MyService -{ - /** @var Cache */ - private $cache; - - public function setCache(Cache $service): void - { - $this->cache = $service; - } -} - -$service = new MyService; -$service->setCache($cache); -``` - -This method is useful for optional dependencies that are not necessary for the class function, since it is not guaranteed that the object will actually receive them (i.e., that the user will call the method). - -At the same time, this method allows the setter to be called repeatedly to change the dependency. If this is not desirable, add a check to the method, or as of PHP 8.1, mark the property `$cache` with the `readonly` flag. - -```php -class MyService -{ - /** @var Cache */ - private $cache; - - public function setCache(Cache $service): void - { - if ($this->cache) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $service; - } -} -``` - -The setter call is defined in the DI container configuration in [section setup |services#Setup]. Also here the automatic passing of dependencies is used by autowiring: - -```neon -services: - - - create: MyService - setup: - - setCache -``` - - -Property Injection -================== - -Dependencies are passed directly to the property: - -```php -class MyService -{ - /** @var Cache */ - public $cache; -} - -$service = new MyService; -$service->cache = $cache; -``` - -This method is considered inappropriate because the property must be declared as `public`. Hence, we have no control over whether the passed dependency will actually be of the specified type (this was true before PHP 7.4) and we lose the ability to react to the newly assigned dependency with our own code, for example to prevent subsequent changes. At the same time, the property becomes part of the public interface of the class, which may not be desirable. - -The setting of the variable is defined in the DI container configuration in [section setup |services#Setup]: - -```neon -services: - - - create: MyService - setup: - - $cache = @\Cache -``` - - -Which Way to Choose? -==================== - -- constructor is suitable for mandatory dependencies that the class needs to function -- the setter, on the other hand, is suitable for optional dependencies, or dependencies that can be changed -- public variables are not recommended diff --git a/dependency-injection/en/services.texy b/dependency-injection/en/services.texy deleted file mode 100644 index 2d6652f681..0000000000 --- a/dependency-injection/en/services.texy +++ /dev/null @@ -1,447 +0,0 @@ -Service Definitions -******************* - -.[perex] -Configuration is where we place the definitions of custom services. This is done in section `services`. - -For example, this is how we create a service named `database`, which will be an instance of class `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -The naming of services is used to allow us to [reference|#Referencing Services] them. If a service is not referenced, there is no need to name it. So we just use a bullet point instead of a name: - -```neon -services: - - PDO('sqlite::memory:') # anonymous service -``` - -A one-line entry can be broken up into multiple lines to allow additional keys to be added, such as [#setup]. The alias for the `create:` key is `factory:`. - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -We then retrieve the service from the DI container using the method `getService()` by name, or better yet, the method `getByType()` by type: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Creating a Service -================== - -Most often, we create a service by simply creating an instance of a class: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Which will generate a factory method in [DI container|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret'); -} -``` - -Alternatively, a key `arguments` can be used to pass [arguments|#Arguments]: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -A static method can also create a service: - -```neon -services: - database: My\Database::create(root, secret) -``` - -Corresponds to PHP code: - -```php -public function createServiceDatabase(): PDO -{ - return My\Database::create('root', 'secret'); -} -``` - -A static method `My\Database::create()` is assumed to have a defined return value that the DI container needs to know. If it does not have it, we write the type to the configuration: - -```neon -services: - database: - create: My\Database::create(root, secret) - type: PDO -``` - -Nette DI gives you extremely powerful expression facilities to write almost anything. For example, to [refer|#Referencing Services] to another service and call its method. For simplicity, `::` is used instead of `->`. - -```neon -services: - routerFactory: App\Router\Factory - router: @routerFactory::create() -``` - -Corresponds to PHP code: - -```php -public function createServiceRouterFactory(): App\Router\Factory -{ - return new App\Router\Factory; -} - -public function createServiceRouter(): Router -{ - return $this->getService('routerFactory')->create(); -} -``` - -Method calls can be chained together as in PHP: - -```neon -services: - foo: FooFactory::build()::get() -``` - -Corresponds to PHP code: - -```php -public function createServiceFoo() -{ - return FooFactory::build()->get(); -} -``` - - -Arguments -========= - -Named parameters can also be used to pass arguments: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' # positional - username: root # named - password: secret # named - ) -``` - -The use of commas is optional when breaking arguments into multiple lines. - -Of course, we can also use [other services|#Referencing Services] or [parameters|configuration#parameters] as arguments: - -```neon -services: - - Foo(@anotherService, %appDir%) -``` - -Corresponds to PHP code: - -```php -public function createService01(): Foo -{ - return new Foo($this->getService('anotherService'), '...'); -} -``` - -If the first argument is [autowired|autowiring] and you want to specify the second one, omit the first one with the `_` character, for example `Foo(_, %appDir%)`. Or better yet, pass only the second argument as a named parameter, e.g. `Foo(path: %appDir%)`. - -Nette DI and the NEON format give you extremely powerful expressive facilities to write almost anything. Thus an argument can be a newly created object, you can call static methods, methods of other services, or even global functions using special notation: - -```neon -services: - analyser: My\Analyser( - FilesystemIterator(%appDir%) # create object - DateTime::createFromFormat('Y-m-d') # call static method - @anotherService # passing another service - @http.request::getRemoteAddress() # calling another service method - ::getenv(NETTE_MODE) # call a global function - ) -``` - -Corresponds to PHP code: - -```php -public function createServiceAnalyser(): My\Analyser -{ - return new My\Analyser( - new FilesystemIterator('...'), - DateTime::createFromFormat('Y-m-d'), - $this->getService('anotherService'), - $this->getService('http.request')->getRemoteAddress(), - getenv('NETTE_MODE') - ); -} -``` - - -Special Functions ------------------ - -You can also use special functions in arguments to cast or negate values: - -- `not(%arg%)` negation -- `bool(%arg%)` lossless cast to bool -- `int(%arg%)` lossless cast to int -- `float(%arg%)` lossless cast to float -- `string(%arg%)` lossless cast to string - -```neon -services: - - Foo( - id: int(::getenv('PROJECT_ID')) - productionMode: not(%debugMode%) - ) -``` - -Lossless rewriting differs from normal PHP rewriting, e.g. using `(int)`, in that it throws an exception for non-numeric values. - -Multiple services can be passed as arguments. An array of all services of a particular type (i.e., class or interface) is created by function `typed()`. The function will omit services that have autowiring disabled, and multiple types separated by a comma can be specified. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -You can also pass an array of services automatically using [autowiring|autowiring#Collection of Services]. - -An array of all services with a certain [tag|#tags] is created by function `tagged()`. Multiple tags separated by a comma can be specified. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Referencing Services -==================== - -Individual services are referenced using character `@` and name, so for example `@database`: - -```neon -services: - - create: Foo(@database) - setup: - - setCacheStorage(@cache.storage) -``` - -Corresponds to PHP code: - -```php -public function createService01(): Foo -{ - $service = new Foo($this->getService('database')); - $service->setCacheStorage($this->getService('cache.storage')); - return $service; -} -``` - -Even anonymous services can be referenced using a callback, just specify their type (class or interface) instead of their name. However, this is usually not necessary due to [autowiring|autowiring]. - -```neon -services: - - create: Foo(@Nette\Database\Connection) # or @\PDO - setup: - - setCacheStorage(@cache.storage) -``` - - -Setup -===== - -In the setup section we list the methods to be called when creating the service: - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Corresponds to PHP code: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Properites can also be set. Adding an element to an array is also supported, and should be written in quotes so as not to conflict with NEON syntax: - - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Corresponds to PHP code: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -However, static methods or methods of other services can also be called in the setup. We pass the actual service to them as `@self`: - - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Corresponds to PHP code: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Autowiring -========== - -The autowired key can be used to exclude a service from autowiring or to influence its behavior. See [chapter on autowiring|autowiring] for more information. - -```neon -services: - foo: - create: Foo - autowired: false # foo is removed from autowiring -``` - - -Tags -==== - -User information can be added to individual services in the form of tags: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Tags can also have a value: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -An array of services with certain tags can be passed as an argument using the function `tagged()`. Multiple tags separated by a comma can also be specified. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -Service names can be obtained from the DI container using the method `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names is an array containing the service name and tag value -// i.e. ['foo' => 'monolog.logger.event', ...] -``` - - -Inject Mode -=========== - -The `inject: true` flag is used to activate the passing of dependencies via public variables with the [inject |best-practices:inject-method-attribute#Inject Annotations] annotation and the [inject*() |best-practices:inject-method-attribute#inject Methods] methods. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -By default, `inject` is only activated for presenters. - - -Modification of Services -======================== - -There are a number of services in the DI container that have been added by built-in or [your extension|#di-extensions]. The definitions of these services can be modified in the configuration. For example, for service `application.application`, which is by default an object `Nette\Application\Application`, we can change the class: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -The `alteration` flag is informative and says that we are just modifying an existing service. - -We can also add a setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -When rewriting a service, we may want to remove the original arguments, setup items or tags, which is what `reset` is for: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -A service added by extension can also be removed from the container: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/meta.json b/dependency-injection/meta.json deleted file mode 100644 index 534033e8c9..0000000000 --- a/dependency-injection/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "3.x", - "repo": "nette/di", - "composer": "nette/di" -} diff --git a/forms/cs/@home.texy b/forms/cs/@home.texy index 70dd5d0105..496012fa21 100644 --- a/forms/cs/@home.texy +++ b/forms/cs/@home.texy @@ -1,5 +1,5 @@ -Formuláře -********* +Nette Forms +*********** <div class=perex> diff --git a/forms/cs/@left-menu.texy b/forms/cs/@left-menu.texy index 6289eb99ec..dbf61e5dbe 100644 --- a/forms/cs/@left-menu.texy +++ b/forms/cs/@left-menu.texy @@ -1,5 +1,5 @@ -Formuláře -********* +Nette Forms +*********** - [Úvod |@home] - [Formuláře v presenterech|in-presenter] - [Formuláře samostatně|standalone] diff --git a/forms/cs/@meta.texy b/forms/cs/@meta.texy new file mode 100644 index 0000000000..462d9add80 --- /dev/null +++ b/forms/cs/@meta.texy @@ -0,0 +1 @@ +{{sitename: Nette Dokumentace}} diff --git a/forms/cs/configuration.texy b/forms/cs/configuration.texy index f76c18103d..313f3faa12 100644 --- a/forms/cs/configuration.texy +++ b/forms/cs/configuration.texy @@ -7,27 +7,27 @@ V konfiguraci lze změnit výchozí [chybové hlášky formulářů|validation]. ```neon forms: messages: - EQUAL: 'Please enter %s.' - NOT_EQUAL: 'This value should not be %s.' - FILLED: 'This field is required.' - BLANK: 'This field should be blank.' - MIN_LENGTH: 'Please enter at least %d characters.' - MAX_LENGTH: 'Please enter no more than %d characters.' - LENGTH: 'Please enter a value between %d and %d characters long.' - EMAIL: 'Please enter a valid email address.' + Equal: 'Please enter %s.' + NotEqual: 'This value should not be %s.' + Filled: 'This field is required.' + Blank: 'This field should be blank.' + MinLength: 'Please enter at least %d characters.' + MaxLength: 'Please enter no more than %d characters.' + Length: 'Please enter a value between %d and %d characters long.' + Email: 'Please enter a valid email address.' URL: 'Please enter a valid URL.' - INTEGER: 'Please enter a valid integer.' - FLOAT: 'Please enter a valid number.' - MIN: 'Please enter a value greater than or equal to %d.' - MAX: 'Please enter a value less than or equal to %d.' - RANGE: 'Please enter a value between %d and %d.' - MAX_FILE_SIZE: 'The size of the uploaded file can be up to %d bytes.' - MAX_POST_SIZE: 'The uploaded data exceeds the limit of %d bytes.' - MIME_TYPE: 'The uploaded file is not in the expected format.' - IMAGE: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' - Nette\Forms\Controls\SelectBox::VALID: 'Please select a valid option.' - Nette\Forms\Controls\UploadControl::VALID: 'An error occurred during file upload.' - Nette\Forms\Controls\CsrfProtection::PROTECTION: 'Your session has expired. Please return to the home page and try again.' + Integer: 'Please enter a valid integer.' + Float: 'Please enter a valid number.' + Min: 'Please enter a value greater than or equal to %d.' + Max: 'Please enter a value less than or equal to %d.' + Range: 'Please enter a value between %d and %d.' + MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' + MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' + MimeType: 'The uploaded file is not in the expected format.' + Image: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' + Nette\Forms\Controls\SelectBox::Valid: 'Please select a valid option.' + Nette\Forms\Controls\UploadControl::Valid: 'An error occurred during file upload.' + Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' ``` Zde je český překlad: @@ -35,27 +35,27 @@ Zde je český překlad: ```neon forms: messages: - EQUAL: 'Zadejte %s.' - NOT_EQUAL: 'Tato hodnota by neměla být %s.' - FILLED: 'Toto pole je povinné.' - BLANK: 'Toto pole by mělo být prázdné.' - MIN_LENGTH: 'Zadejte prosím alespoň %d znaků.' - MAX_LENGTH: 'Zadejte prosím maximálně %d znaků.' - LENGTH: 'Zadejte prosím hodnotu %d až %d znaků dlouho.' - EMAIL: 'Zadejte platnou e-mailovou adresu.' + Equal: 'Zadejte %s.' + NotEqual: 'Tato hodnota by neměla být %s.' + Filled: 'Toto pole je povinné.' + Blank: 'Toto pole by mělo být prázdné.' + MinLength: 'Zadejte prosím alespoň %d znaků.' + MaxLength: 'Zadejte prosím maximálně %d znaků.' + Length: 'Zadejte prosím hodnotu %d až %d znaků dlouho.' + Email: 'Zadejte platnou e-mailovou adresu.' URL: 'Zadejte prosím platné URL.' - INTEGER: 'Zadejte platné celé číslo.' - FLOAT: 'Zadejte platné číslo.' - MIN: 'Zadejte prosím hodnotu větší nebo rovnou %d.' - MAX: 'Zadejte prosím hodnotu menší nebo rovnou %d.' - RANGE: 'Zadejte hodnotu mezi %d a %d.' - MAX_FILE_SIZE: 'Velikost nahraného souboru může být nejvýše %d bytů.' - MAX_POST_SIZE: 'Nahraná data překračují limit %d bytů.' - MIME_TYPE: 'Nahraný soubor není v očekávaném formátu.' - IMAGE: 'Nahraný soubor musí být obraz ve formátu JPEG, GIF, PNG nebo WebP.' - Nette\Forms\Controls\SelectBox::VALID: 'Vyberte prosím platnou možnost.' - Nette\Forms\Controls\UploadControl::VALID: 'Při nahrávání souboru došlo k chybě.' - Nette\Forms\Controls\CsrfProtection::PROTECTION: 'Vaše relace vypršela. Vraťte se na domovskou stránku a zkuste to znovu.' + Integer: 'Zadejte platné celé číslo.' + Float: 'Zadejte platné číslo.' + Min: 'Zadejte prosím hodnotu větší nebo rovnou %d.' + Max: 'Zadejte prosím hodnotu menší nebo rovnou %d.' + Range: 'Zadejte hodnotu mezi %d a %d.' + MaxFileSize: 'Velikost nahraného souboru může být nejvýše %d bytů.' + MaxPostSize: 'Nahraná data překračují limit %d bytů.' + MimeType: 'Nahraný soubor není v očekávaném formátu.' + Image: 'Nahraný soubor musí být obraz ve formátu JPEG, GIF, PNG, WebP nebo AVIF.' + Nette\Forms\Controls\SelectBox::Valid: 'Vyberte prosím platnou možnost.' + Nette\Forms\Controls\UploadControl::Valid: 'Při nahrávání souboru došlo k chybě.' + Nette\Forms\Controls\CsrfProtection::Protection: 'Vaše relace vypršela. Vraťte se na domovskou stránku a zkuste to znovu.' ``` Pokud nepoužívate celý framework a tedy ani konfigurační soubory, můžete změnit výchozí chybové hlášky přímo v poli `Nette\Forms\Validator::$messages`. diff --git a/forms/cs/controls.texy b/forms/cs/controls.texy index ad27b81709..c8262c9a9d 100644 --- a/forms/cs/controls.texy +++ b/forms/cs/controls.texy @@ -5,8 +5,8 @@ Formulářové prvky Přehled standardních formulářových prvků. -addText(string|int $name, $label=null): TextInput .[method] -=========================================================== +addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== Přidá jednořádkové textové políčko (třída [TextInput |api:Nette\Forms\Controls\TextInput]). Pokud uživatel pole nevyplní, vrací prázdný řetězec `''`, nebo pomocí `setNullable()` lze určit, aby vracel `null`. @@ -18,15 +18,12 @@ $form->addText('name', 'Jméno:') Automaticky validuje UTF-8, ořezává levo- a pravostranné mezery a odstraňuje odřádkování, které by mohl odeslat útočník. -Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter()|validation#Úprava vstupu]. +Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter() |validation#Úprava vstupu]. -Pomocí `setHtmlType()` lze změnit [charakter|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types] vstupního prvku na `search`, `tel`, `url`, `range`, `date`, `datetime-local`, `month`, `time`, `week`, `color`. Místo typů `number` a `email` doporučujeme použít [#addInteger] a [#addEmail], které disponují validací na straně serveru. +Pomocí `setHtmlType()` lze změnit vizuální charakter textového pole na typy jako `search`, `tel` nebo `url` viz [specifikace|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Pamatujte, že změna typu je pouze vizuální a nezastupuje funkci validace. Pro typ `url` je vhodné přidat specifické validační [pravidlo URL |validation#Textové vstupy]. -```php -$form->addText('color', 'Vyberte barvu:') - ->setHtmlType('color') - ->addRule($form::PATTERN, 'invalid value', '[0-9a-f]{6}'); -``` +.[note] +Pro další typy vstupů, jako `number`, `range`, `email`, `date`, `datetime-local`, `time` a `color`, použijte specializované metody jako [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] a [#addColor], které zajišťují serverovou validaci. Typy `month` a `week` zatím nejsou plně podporovány ve všech prohlížečích. Prvku lze nastavit tzv. empty-value, což je něco jako výchozí hodnota, ale pokud ji uživatel nezmění, vrátí prvek prázdný řetězec či `null`. @@ -44,12 +41,12 @@ Přidá pole pro zadání víceřádkového textu (třída [TextArea |api:Nette\ ```php $form->addTextArea('note', 'Poznámka:') - ->addRule($form::MAX_LENGTH, 'Poznámka je příliš dlouhá', 10000); + ->addRule($form::MaxLength, 'Poznámka je příliš dlouhá', 10000); ``` Automaticky validuje UTF-8 a normalizuje oddělovače řádků na `\n`. Na rozdíl od jednořádkového vstupního políčka k žádnému ořezávání mezer nedochází. -Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter()|validation#Úprava vstupu]. Lze nastavit tzv. empty-value pomocí `setEmptyValue()`. +Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter() |validation#Úprava vstupu]. Lze nastavit tzv. empty-value pomocí `setEmptyValue()`. addInteger(string|int $name, $label=null): TextInput .[method] @@ -58,14 +55,31 @@ addInteger(string|int $name, $label=null): TextInput .[method] Přidá políčko pro zadání celočíselného čísla (třída [TextInput |api:Nette\Forms\Controls\TextInput]). Vrací buď integer, nebo `null`, pokud uživatel nic nezadá. ```php -$form->addInteger('level', 'Úroveň:') +$form->addInteger('year', 'Rok:') + ->addRule($form::Range, 'Rok musí být v rozsahu od %d do %d.', [1900, 2023]); +``` + +Prvek se vykresluje jako `<input type="number">`. Použitím metody `setHtmlType()` lze změnit typ na `range` pro zobrazení v podobě posuvníku, nebo na `text`, pokud preferujete standardní textové pole bez speciálního chování typu `number`. + + +addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} +================================================================================= + +Přidá políčko pro zadání desetinného čísla (třída [TextInput |api:Nette\Forms\Controls\TextInput]). Vrací buď float, nebo `null`, pokud uživatel nic nezadá. + +```php +$form->addFloat('level', 'Úroveň:') ->setDefaultValue(0) - ->addRule($form::RANGE, 'Úroveň musí být v rozsahu mezi %d a %d.', [0, 100]); + ->addRule($form::Range, 'Úroveň musí být v rozsahu od %d do %d.', [0, 100]); ``` +Prvek se vykresluje jako `<input type="number">`. Použitím metody `setHtmlType()` lze změnit typ na `range` pro zobrazení v podobě posuvníku, nebo na `text`, pokud preferujete standardní textové pole bez speciálního chování typu `number`. + +Nette a prohlížeč Chrome akceptují jako oddělovač desetinných míst jak čárku, tak tečku. Aby byla tato funkcionalita dostupná i ve Firefoxu, je doporučeno nastavit atribut `lang` buď pro daný prvek nebo pro celou stránku, například `<html lang="cs">`. -addEmail(string|int $name, $label=null): TextInput .[method] -============================================================ + +addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] +================================================================================ Přidá políčko pro zadání e-mailové adresy (třída [TextInput |api:Nette\Forms\Controls\TextInput]). Pokud uživatel pole nevyplní, vrací prázdný řetězec `''`, nebo pomocí `setNullable()` lze určit, aby vracel `null`. @@ -75,19 +89,19 @@ $form->addEmail('email', 'E-mail:'); Ověří, zda je hodnota platná e-mailová adresa. Neověřuje se, zda doména skutečně existuje, ověřuje se pouze syntaxe. Automaticky validuje UTF-8, ořezává levo- a pravostranné mezery. -Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter()|validation#Úprava vstupu]. Lze nastavit tzv. empty-value pomocí `setEmptyValue()`. +Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter() |validation#Úprava vstupu]. Lze nastavit tzv. empty-value pomocí `setEmptyValue()`. -addPassword(string|int $name, $label=null): TextInput .[method] -=============================================================== +addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +====================================================================================================== Přidá políčko pro zadání hesla (třída [TextInput |api:Nette\Forms\Controls\TextInput]). ```php $form->addPassword('password', 'Heslo:') ->setRequired() - ->addRule($form::MIN_LENGTH, 'Heslo musí mít alespoň %d znaků', 8) - ->addRule($form::PATTERN, 'Musí obsahovat číslici', '.*[0-9].*'); + ->addRule($form::MinLength, 'Heslo musí mít alespoň %d znaků', 8) + ->addRule($form::Pattern, 'Musí obsahovat číslici', '.*[0-9].*'); ``` Při znovuzobrazení formuláře bude políčko prázdné. Automaticky validuje UTF-8, ořezává levo- a pravostranné mezery a odstraňuje odřádkování, které by mohl odeslat útočník. @@ -104,8 +118,8 @@ $form->addCheckbox('agree', 'Souhlasím s podmínkami') ``` -addCheckboxList(string|int $name, $label=null, array $items=null): CheckboxList .[method] -========================================================================================= +addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] +========================================================================================== Přidá zaškrtávací políčka pro výběr více položek (třída [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Vrací pole klíčů vybraných položek. Metoda `getSelectedItems()` vrací hodnoty místo klíčů. @@ -125,9 +139,15 @@ Prvek automaticky kontroluje, že nedošlo k podvržení a že vybrané položky Při nastavení výchozích vybraných položek také kontroluje, že jde o jedny z nabízených, jinak vyhodí výjimku. Tuto kontrolu lze vypnout pomocí `checkDefaultValue(false)`. +Pokud odesíláte formulář metodou `GET`, můžete zvolit kompaktnější způsob přenosu dat, který šetří velikost query stringu. Aktivuje se nastavením HTML atributu formuláře: + +```php +$form->setHtmlAttribute('data-nette-compact'); +``` -addRadioList(string|int $name, $label=null, array $items=null): RadioList .[method] -=================================================================================== + +addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] +==================================================================================== Přidá přepínací tlačítka (třída [RadioList |api:Nette\Forms\Controls\RadioList]). Vrací klíč vybrané položky, nebo `null`, pokud uživatel nic nevybral. Metoda `getSelectedItem()` vrací hodnotu místo klíče. @@ -148,8 +168,8 @@ Prvek automaticky kontroluje, že nedošlo k podvržení a že vybraná položka Při nastavení výchozí vybrané položky také kontroluje, že jde o jednou z nabízených, jinak vyhodí výjimku. Tuto kontrolu lze vypnout pomocí `checkDefaultValue(false)`. -addSelect(string|int $name, $label=null, array $items=null): SelectBox .[method] -================================================================================ +addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +================================================================================================== Přidá select box (třída [SelectBox |api:Nette\Forms\Controls\SelectBox]). Vrací klíč vybrané položky, nebo `null`, pokud uživatel nic nevybral. Metoda `getSelectedItem()` vrací hodnotu místo klíče. @@ -193,8 +213,8 @@ Prvek automaticky kontroluje, že nedošlo k podvržení a že vybraná položka Při nastavení výchozí vybrané položky také kontroluje, že jde o jednou z nabízených, jinak vyhodí výjimku. Tuto kontrolu lze vypnout pomocí `checkDefaultValue(false)`. -addMultiSelect(string|int $name, $label=null, array $items=null): MultiSelectBox .[method] -========================================================================================== +addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +============================================================================================================ Přidá select box pro výběr více položek (třída [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Vrací pole klíčů vybraných položek. Metoda `getSelectedItems()` vrací hodnoty místo klíčů. @@ -214,40 +234,113 @@ Při nastavení výchozích vybraných položek také kontroluje, že jde o jedn addUpload(string|int $name, $label=null): UploadControl .[method] ================================================================= -Přidá políčko pro upload souboru (třída [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrací objekt [FileUpload|http:request#FileUpload] a to i v případě, že uživatel žádný soubor neodeslal, což lze zjistit metodou `FileUpload::hasFile()`. +Přidá políčko pro upload souboru (třída [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrací objekt [FileUpload |http:request#FileUpload] a to i v případě, že uživatel žádný soubor neodeslal, což lze zjistit metodou `FileUpload::hasFile()`. ```php $form->addUpload('avatar', 'Avatar:') - ->addRule($form::IMAGE, 'Avatar musí být JPEG, PNG, GIF or WebP.') - ->addRule($form::MAX_FILE_SIZE, 'Maximální velikost je 1 MB.', 1024 * 1024); + ->addRule($form::Image, 'Avatar musí být JPEG, PNG, GIF, WebP or AVIF.') + ->addRule($form::MaxFileSize, 'Maximální velikost je 1 MB.', 1024 * 1024); ``` Pokud se soubor nepodaří korektně nahrát, formulář není úspěšně odeslaný a zobrazí se chyba. Tj. při úspěšném odeslání není potřeba ověřovat metodu `FileUpload::isOk()`. Nikdy nevěřte originálnímu názvu souboru vráceného metodou `FileUpload::getName()`, klient mohl odeslat škodlivý název souboru s úmyslem poškodit nebo hacknout vaši aplikaci. -Pravidla `MIME_TYPE` a `IMAGE` detekují požadovaný typ na základě signatury souboru a neověřují jeho integritu. Zda není obrázek poškozený lze zjistit například pokusem o jeho [načtení|http:request#toImage]. +Pravidla `MimeType` a `Image` detekují požadovaný typ na základě signatury souboru a neověřují jeho integritu. Zda není obrázek poškozený lze zjistit například pokusem o jeho [načtení |http:request#toImage]. addMultiUpload(string|int $name, $label=null): UploadControl .[method] ====================================================================== -Přidá políčko pro upload více souboru najednou (třída [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrací pole objektů [FileUpload|http:request#FileUpload]. Metoda `FileUpload::hasFile()` u každého z nich bude vracet `true`. +Přidá políčko pro upload více souboru najednou (třída [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrací pole objektů [FileUpload |http:request#FileUpload]. Metoda `FileUpload::hasFile()` u každého z nich bude vracet `true`. ```php $form->addMultiUpload('files', 'Soubory:') - ->addRule($form::MAX_LENGTH, 'Maximálně lze nahrát %d souborů', 10); + ->addRule($form::MaxLength, 'Maximálně lze nahrát %d souborů', 10); ``` Pokud se některý soubor nepodaří korektně nahrát, formulář není úspěšně odeslaný a zobrazí se chyba. Tj. při úspěšném odeslání není potřeba ověřovat metodu `FileUpload::isOk()`. Nikdy nevěřte originálním názvům souborů vráceným metodou `FileUpload::getName()`, klient mohl odeslat škodlivý název souboru s úmyslem poškodit nebo hacknout vaši aplikaci. -Pravidla `MIME_TYPE` a `IMAGE` detekují požadovaný typ na základě signatury souboru a neověřují jeho integritu. Zda není obrázek poškozený lze zjistit například pokusem o jeho [načtení|http:request#toImage]. +Pravidla `MimeType` a `Image` detekují požadovaný typ na základě signatury souboru a neověřují jeho integritu. Zda není obrázek poškozený lze zjistit například pokusem o jeho [načtení |http:request#toImage]. + + +addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +====================================================================================== + +Přidá políčko, které umožní uživateli snadno zadat datum skládající se z roku, měsíce a dne (třída [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). + +Jako výchozí hodnotu akceptuje buď objekty implementující rozhraní `DateTimeInterface`, řetězec s časem, nebo číslo představující UNIX timestamp. Totéž platí pro argumenty pravidel `Min`, `Max` nebo `Range`, jež definují minimální a maximální povolený datum. + +```php +$form->addDate('date', 'Datum:') + ->setDefaultValue(new DateTime) + ->addRule($form::Min, 'Datum musí být minimálně měsíc staré.', new DateTime('-1 month')); +``` + +Standardně vrací objekt `DateTimeImmutable`, metodou `setFormat()` můžete specifikovat [textový formát|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] či timestamp: +```php +$form->addDate('date', 'Datum:') + ->setFormat('Y-m-d'); +``` + + +addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== + +Přidá políčko, které umožní uživateli snadno zadat čas skládající se z hodin, minut a volitelně i sekund (třída [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). + +Jako výchozí hodnotu akceptuje buď objekty implementující rozhraní `DateTimeInterface`, řetězec s časem, nebo číslo představující UNIX timestamp. Z těchto vstupů je využita pouze časová informace, datum je ignorováno. Totéž platí pro argumenty pravidel `Min`, `Max` nebo `Range`, jež definují minimální a maximální povolený čas. Pokud je nastavená minimální hodnota vyšší než maximální, vytvoří se časový rozsah přesahující půlnoc. + +```php +$form->addTime('time', 'Čas:', withSeconds: true) + ->addRule($form::Range, 'Čas musí být v rozsahu od %d do %d.', ['12:30', '13:30']); +``` -addHidden(string|int $name, string $default=null): HiddenField .[method] -======================================================================== +Standardně vrací objekt `DateTimeImmutable` (s datem 1. ledna roku 1), metodou `setFormat()` můžete specifikovat [textový formát|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: + +```php +$form->addTime('time', 'Čas:') + ->setFormat('H:i'); +``` + + +addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=================================================================================================================== + +Přidá políčko, které umožní uživateli snadno zadat datum a čas skládající se z roku, měsíce, dne, hodin, minut a volitelně i sekund (třída [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). + +Jako výchozí hodnotu akceptuje buď objekty implementující rozhraní `DateTimeInterface`, řetězec s časem, nebo číslo představující UNIX timestamp. Totéž platí pro argumenty pravidel `Min`, `Max` nebo `Range`, jež definují minimální a maximální povolený datum. + +```php +$form->addDateTime('datetime', 'Datum a čas:') + ->setDefaultValue(new DateTime) + ->addRule($form::Min, 'Datum musí být minimálně měsíc staré.', new DateTime('-1 month')); +``` + +Standardně vrací objekt `DateTimeImmutable`, metodou `setFormat()` můžete specifikovat [textový formát|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] či timestamp: + +```php +$form->addDateTime('datetime') + ->setFormat(DateTimeControl::FormatTimestamp); +``` + + +addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=================================================================================== + +Přidá políčko pro výběr barvy (třída [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Barva je řetězec ve tvaru `#rrggbb`. Pokud uživatel volbu neprovede, vrátí se černá barva `#000000`. + +```php +$form->addColor('color', 'Barva:') + ->setDefaultValue('#3C8ED7'); +``` + + +addHidden(string|int $name, ?string $default=null): HiddenField .[method] +========================================================================= Přidá skryté pole (třída [HiddenField |api:Nette\Forms\Controls\HiddenField]). @@ -255,7 +348,9 @@ Přidá skryté pole (třída [HiddenField |api:Nette\Forms\Controls\HiddenField $form->addHidden('userid'); ``` -Pomocí `setNullable()` lze nastavit, aby vracel `null` místo prázdného řetězce. Pozměnit odeslanou hodnotu umožňuje [addFilter()|validation#Úprava vstupu]. +Pomocí `setNullable()` lze nastavit, aby vracel `null` místo prázdného řetězce. Pozměnit odeslanou hodnotu umožňuje [addFilter() |validation#Úprava vstupu]. + +Ačkoli je prvek skrytý, je **důležité si uvědomit**, že hodnota může být stále modifikována nebo podvržena útočníkem. Vždy důkladně ověřujte a validujte všechny přijaté hodnoty na serverové straně, aby se předešlo bezpečnostním rizikům spojeným s manipulací dat. addSubmit(string|int $name, $caption=null): SubmitButton .[method] @@ -282,7 +377,7 @@ if ($form['register']->isSubmittedBy()) { } ``` -Pokud nechcete validovat celý formulář při stisknutí tlačítka (například u tlačítek *Zrušit* nebo *Náhled*), použijte [setValidationScope()|validation#Vypnutí validace]. +Pokud nechcete validovat celý formulář při stisknutí tlačítka (například u tlačítek *Zrušit* nebo *Náhled*), použijte [setValidationScope() |validation#Vypnutí validace]. addButton(string|int $name, $caption): Button .[method] @@ -296,8 +391,8 @@ $form->addButton('raise', 'Zvýšit plat') ``` -addImageButton(string|int $name, string $src=null, string $alt=null): ImageButton .[method] -=========================================================================================== +addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +============================================================================================= Přidá odesílací tlačítko v podobě obrázku (třída [ImageButton |api:Nette\Forms\Controls\ImageButton]). @@ -353,19 +448,19 @@ U všech prvků můžeme volat následující metody (kompletní přehled v [API Vykreslování: .[table-form-methods language-php] | `setCaption($caption)` | změní popisku prvku -| `setTranslator($translator)` | nastaví [překladač|best-practices:translations#Překlad formulářů] +| `setTranslator($translator)` | nastaví [překladač |rendering#Překládání] | `setHtmlAttribute($name, $value)` | nastaví [HTML atribut |rendering#HTML atributy] elementu | `setHtmlId($id)` | nastaví HTML atribut `id` | `setHtmlType($type)` | nastaví HTML atribut `type` | `setHtmlName($name)` | nastaví HTML atribut `name` -| `setOption($key, $value)` | [nastavení pro vykreslování|rendering#Options] +| `setOption($key, $value)` | [nastavení pro vykreslování |rendering#Options] Validace: .[table-form-methods language-php] | `setRequired()` | [povinný prvek |validation] -| `addRule()` | nastavení [validační pravidlo |validation#Pravidla] -| `addCondition()`, `addConditionOn()` | nastaví [validační podmínku|validation#Podmínky] -| `addError($message)` | [předání chybové zprávy|validation#chyby-pri-zpracovani] +| `addRule()` | nastaví [validační pravidlo |validation#Pravidla] +| `addCondition()`, `addConditionOn()` | nastaví [validační podmínku |validation#Podmínky] +| `addError($message)` | [předání chybové zprávy |validation#Chyby při zpracování] U prvků `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` lze volat následující metody: @@ -377,20 +472,20 @@ U prvků `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addIntege Vynechání hodnoty ------------------ +================= Pokud nás uživatelem vyplněná hodnota nezajímá, můžeme ji pomocí `setOmitted()` vynechat z výsledku metody `$form->getValues()` nebo z dat předávaných do handlerů. To se hodí pro různá hesla pro kontrolu, antispamové prvky atd. ```php $form->addPassword('passwordVerify', 'Heslo pro kontrolu:') ->setRequired('Zadejte prosím heslo ještě jednou pro kontrolu') - ->addRule($form::EQUAL, 'Hesla se neshodují', $form['password']) + ->addRule($form::Equal, 'Hesla se neshodují', $form['password']) ->setOmitted(); ``` Deaktivace prvků ----------------- +================ Prvky lze deaktivovat pomocí `setDisabled()`. Takový prvek nemůže uživatel editovat. @@ -399,9 +494,9 @@ $form->addText('username', 'Uživatelské jméno:') ->setDisabled(); ``` -Upozorňujeme, že disablované prvky prohlížeč vůbec neodesílá na server, tedy je ani nenajdete v datech vrácených funkcí `$form->getValues()`. +Disablované prvky prohlížeč vůbec neodesílá na server, tedy je ani nenajdete v datech vrácených funkcí `$form->getValues()`. Pokud však nastavíte `setOmitted(false)`, Nette do těchto dat zahrne jejich výchozí hodnotu. -Pokud prvku nastavujete výchozí hodnotu, je tak nutné učinit až po jeho deaktivaci: +Při volání `setDisabled()` se z bezpečnostních důvodů **smaže hodnota prvku**. Pokud nastavujete výchozí hodnotu, je tak nutné učinit až po jeho deaktivaci: ```php $form->addText('username', 'Uživatelské jméno:') @@ -409,6 +504,8 @@ $form->addText('username', 'Uživatelské jméno:') ->setDefaultValue($userName); ``` +Alternativou disablovaných prvků jsou prvky s HTML atributem `readonly`, které prohlížeč na server posílá. Ačkoliv je prvek pouze pro čtení, je **důležité si uvědomit**, že jeho hodnota může být stále modifikována nebo podvržena útočníkem. + Vlastní prvky ============= @@ -420,15 +517,18 @@ $form->addComponent(new DateInput('Datum:'), 'date'); // alternativní syntax: $form['date'] = new DateInput('Datum:'); ``` +.[note] +Formulář je potomkem třídy [Container |component-model:#Container] a jednotlivé prvky jsou potomky [Component |component-model:#Component]. + Existuje způsob, jak definovat nové metody formuláře sloužící k přidávání vlastních prvků (např. `$form->addZip()`). Jde o tzv. extension methods. Nevýhoda je, že pro ně nebude fungovat napovídání v editorech. ```php use Nette\Forms\Container; -// přidáme metodu addZip(string $name, string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, string $label = null) { +// přidáme metodu addZip(string $name, ?string $label = null) +Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { return $form->addText($name, $label) - ->addRule($form::PATTERN, 'Alespoň 5 čísel', '[0-9]{5}'); + ->addRule($form::Pattern, 'Alespoň 5 čísel', '[0-9]{5}'); }); // použití @@ -450,27 +550,10 @@ Lze používat i prvky, které zapíšeme pouze v šabloně a nepřidáme je do A po odeslání hodnotu zjistíme: ```php -$data = $form->getHttpData($form::DATA_TEXT, 'sel[]'); -$data = $form->getHttpData($form::DATA_TEXT | $form::DATA_KEYS, 'sel[]'); +$data = $form->getHttpData($form::DataText, 'sel[]'); +$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); ``` -kde první parametr je typ elementu (`DATA_FILE` pro `type=file`, `DATA_LINE` pro jednořádkové vstupy jako `text`, `password`, `email` apod. a `DATA_TEXT` pro všechny ostatní) a druhý parametr `sel[]` odpovídá HTML atributu name. Typ elementu můžeme kombinovat s hodnotou `DATA_KEYS`, která zachová klíče prvků. To se hodí zejména pro `select`, `radioList` a `checkboxList`. +kde první parametr je typ elementu (`DataFile` pro `type=file`, `DataLine` pro jednořádkové vstupy jako `text`, `password`, `email` apod. a `DataText` pro všechny ostatní) a druhý parametr `sel[]` odpovídá HTML atributu name. Typ elementu můžeme kombinovat s hodnotou `DataKeys`, která zachová klíče prvků. To se hodí zejména pro `select`, `radioList` a `checkboxList`. Podstatné je, že `getHttpData()` vrací sanitizovanou hodnotu, v tomto případě to bude vždy pole validních UTF-8 řetězců, ať už by se pokusil útočník serveru podstrčit cokoliv. Jde o obdobu přímé práce s `$_POST` nebo `$_GET` avšak s tím podstatným rozdílem, že vždy vrací čistá data, tak, jak jste zvyklí u standardních prvků Nette formulářů. - - - -/--comment -Prvky je rovněž možné přidat pomocí metody [setItems() |api:Nette\Forms\Controls\SelectBox::setItems()]. Pokud chceme místo klíčů položek získat přímo jejich hodnoty, můžeme toho docílit druhým argumentem: - -```php -$form->addSelect('country', 'Země:') - ->setItems($countries, false); -``` - -```php -// pro vypsání možností do 1 řádku -$form->addRadioList('gender', 'Pohlaví:', $sex) - ->getSeparatorPrototype()->setName(null); -``` -\-- diff --git a/forms/cs/in-presenter.texy b/forms/cs/in-presenter.texy index 28f7d45f2c..33ac7fa95d 100644 --- a/forms/cs/in-presenter.texy +++ b/forms/cs/in-presenter.texy @@ -30,11 +30,11 @@ Formulář v presenteru je objekt třídy `Nette\Application\UI\Form`, její př Z pohledu presenteru je formulář běžná komponenta. Proto se s ním jako s komponentou zachází a začleníme ji do presenteru pomocí [tovární metody |application:components#Tovární metody]. Bude to vypadat takto: -```php .{file:app/Presenters/HomepagePresenter.php} +```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; use Nette\Application\UI\Form; -class HomepagePresenter extends Nette\Application\UI\Presenter +class HomePresenter extends Nette\Application\UI\Presenter { protected function createComponentRegistrationForm(): Form { @@ -52,28 +52,26 @@ class HomepagePresenter extends Nette\Application\UI\Presenter // $data->name obsahuje jméno // $data->password obsahuje heslo $this->flashMessage('Byl jste úspěšně registrován.'); - $this->redirect('Homepage:'); + $this->redirect('Home:'); } } ``` A v šabloně formulář vykreslíme značkou `{control}`: -```latte .{file:app/Presenters/templates/Homepage/default.latte} +```latte .{file:app/Presentation/Home/default.latte} <h1>Registrace</h1> {control registrationForm} ``` -A to je vlastně vše :-) Máme funkční a perfektně [zabezpečený|#Ochrana před zranitelnostmi] formulář. +A to je vlastně vše :-) Máme funkční a perfektně [zabezpečený |#Ochrana před zranitelnostmi] formulář. A teď si nejspíš říkáte, že to bylo moc hrr, přemýšlíte, jak je možné, že se zavolá metoda `formSucceeded()` a co jsou parametry, které dostává. Jistě, máte pravdu, tohle si zaslouží vysvětlení. -Nette totiž přichází se svěžím mechanismem, kterému říkáme [Hollywood style|application:components#Hollywood style]. Místo toho, abyste se jako vývojář musel neustále vyptávat, jestli se něco událo („byl formulář odeslaný?“, „byl odeslaný validně?“ a „nedošlo k jeho podvržení?“), řeknete frameworku „až bude formulář validně vyplněný, zavolej tuhle metodu“ a necháte další práci na něm. Pokud programujete v JavaScriptu, tento styl programování důvěrně znáte. Píšete funkce, které se volají, až nastane určitá [událost|nette:glossary#Události]. A jazyk jim předává příslušné argumenty. +Nette totiž přichází se svěžím mechanismem, kterému říkáme [Hollywood style |application:components#Hollywood style]. Místo toho, abyste se jako vývojář musel neustále vyptávat, jestli se něco událo („byl formulář odeslaný?“, „byl odeslaný validně?“ a „nedošlo k jeho podvržení?“), řeknete frameworku „až bude formulář validně vyplněný, zavolej tuhle metodu“ a necháte další práci na něm. Pokud programujete v JavaScriptu, tento styl programování důvěrně znáte. Píšete funkce, které se volají, až nastane určitá [událost |nette:glossary#události]. A jazyk jim předává příslušné argumenty. -Právě takhle je postaven i výše uvedený kód presenteru. Pole `$form->onSuccess` představuje seznam PHP callbacků, které Nette zavolá v okamžiku, kdy je formulář odeslán a správně vyplněn (tj. je validní). -V rámci [životního cyklu presenteru |application:presenters#zivotni-cyklus-presenteru] jde o tzv. signál, volají se tedy po `action*` metodě a před `render*` metodou. -A každému callbacku předá jako první parametr samotný formulář a jako druhý odeslaná data v podobě objektu [ArrayHash |utils:arrays#ArrayHash]. První parametr můžete vynechat, pokud objekt formuláře nepotřebujete. A druhý parametr umí být mazanější, ale o tom až [později|#Mapování na třídy]. +Právě takhle je postaven i výše uvedený kód presenteru. Pole `$form->onSuccess` představuje seznam PHP callbacků, které Nette zavolá v okamžiku, kdy je formulář odeslán a správně vyplněn (tj. je validní). V rámci [životního cyklu presenteru |application:presenters#Životní cyklus presenteru] jde o tzv. signál, volají se tedy po `action*` metodě a před `render*` metodou. A každému callbacku předá jako první parametr samotný formulář a jako druhý odeslaná data v podobě objektu [ArrayHash |utils:arrays#ArrayHash]. První parametr můžete vynechat, pokud objekt formuláře nepotřebujete. A druhý parametr umí být mazanější, ale o tom až [později |#Mapování na třídy]. Objekt `$data` obsahuje klíče `name` a `password` s údaji, které vyplnil uživatel. Obvykle data rovnou posíláme k dalšímu zpracování, což může být například vložení do databáze. Během zpracování se ale může objevit chyba, například uživatelské jméno už je obsazené. V takovém případě chybu předáme zpět do formuláře pomocí `addError()` a necháme jej vykreslit znovu, i s chybovou hláškou. @@ -128,11 +126,10 @@ Zkuste si odeslat formulář bez vyplněného jména a uvidíte, že se zobrazí Zároveň systém neošidíte tím, že do políčka napíšete třeba jen mezery. Kdepak. Nette levo- i pravostranné mezery automaticky odstraňuje. Vyzkoušejte si to. Je to věc, kterou byste měli s každým jednořádkovým inputem vždy udělat, ale často se na to zapomíná. Nette to dělá automaticky. (Můžete zkusit ošálit formulář a jako jméno poslat víceřádkový řetězec. Ani tady se Nette nenechá zmást a odřádkování změní na mezery.) -Formulář se vždy validuje na straně serveru, ale také se generuje JavaScriptová validace, která proběhne bleskově a uživatel se o chybě dozví okamžitě, bez nutnosti formulář odesílat na server. Tohle má na starosti skript `netteForms.js`. -Vložte jej do šablony layoutu: +Formulář se vždy validuje na straně serveru, ale také se generuje JavaScriptová validace, která proběhne bleskově a uživatel se o chybě dozví okamžitě, bez nutnosti formulář odesílat na server. Tohle má na starosti skript `netteForms.js`. Vložte jej do šablony layoutu: ```latte -<script src="https://nette.github.io/resources/js/3/netteForms.min.js"></script> +<script src="https://unpkg.com/nette-forms@3"></script> ``` Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si všimnout, že Nette povinné prvky vkládá do elementů s CSS třídou `required`. Zkuste přidat do šablony následující stylopis a popiska „Jméno“ bude červená. Elegantně tak uživatelům vyznačíme povinné prvky: @@ -145,36 +142,36 @@ Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si v Další validační pravidla přidáme metodou `addRule()`. První parametr je pravidlo, druhý je opět text chybové hlášky a může ještě následovat argument validačního pravidla. Co se tím myslí? -Formulář rozšíříme o nové nepovinné políčko „věk“, které musí být celé číslo (`addInteger()`) a navíc v povoleném rozsahu (`$form::RANGE`). A zde právě využijeme třetí parametr metody `addRule()`, kterým předáme validátoru požadovaný rozsah jako dvojici `[od, do]`: +Formulář rozšíříme o nové nepovinné políčko „věk“, které musí být celé číslo (`addInteger()`) a navíc v povoleném rozsahu (`$form::Range`). A zde právě využijeme třetí parametr metody `addRule()`, kterým předáme validátoru požadovaný rozsah jako dvojici `[od, do]`: ```php $form->addInteger('age', 'Věk:') - ->addRule($form::RANGE, 'Věk musí být od 18 do 120', [18, 120]); + ->addRule($form::Range, 'Věk musí být od 18 do 120', [18, 120]); ``` .[tip] Pokud uživatel políčko nevyplní, nebudou se validační pravidla ověřovat, neboť prvek je nepovinný. -Zde vzniká prostor pro drobný refactoring. V chybové hlášce a ve třetím parametru jsou čísla uvedená duplicitně, což není ideální. Pokud bychom tvořili [vícejazyčné formuláře |best-practices:translations] a hláška obsahující čísla by byla přeložena do více jazyků, ztížila by se případná změna hodnot. Z toho důvodu je možné použít zástupné znaky `%d` a Nette hodnoty doplní: +Zde vzniká prostor pro drobný refactoring. V chybové hlášce a ve třetím parametru jsou čísla uvedená duplicitně, což není ideální. Pokud bychom tvořili [vícejazyčné formuláře |rendering#Překládání] a hláška obsahující čísla by byla přeložena do více jazyků, ztížila by se případná změna hodnot. Z toho důvodu je možné použít zástupné znaky `%d` a Nette hodnoty doplní: ```php - ->addRule($form::RANGE, 'Věk musí být od %d do %d let', [18, 120]); + ->addRule($form::Range, 'Věk musí být od %d do %d let', [18, 120]); ``` -Vraťme se k prvku `password`, který taktéž učiníme povinným a ještě ověříme minimální délku hesla (`$form::MIN_LENGTH`), opět s využitím zástupného znaku: +Vraťme se k prvku `password`, který taktéž učiníme povinným a ještě ověříme minimální délku hesla (`$form::MinLength`), opět s využitím zástupného znaku: ```php $form->addPassword('password', 'Heslo:') ->setRequired('Zvolte si heslo') - ->addRule($form::MIN_LENGTH, 'Heslo musí mít alespoň %d znaků', 8); + ->addRule($form::MinLength, 'Heslo musí mít alespoň %d znaků', 8); ``` -Přidáme do formuláře ještě políčko `passwordVerify`, kde uživatel zadá heslo ještě jednou, pro kontrolu. Pomocí validačních pravidel zkontrolujeme, zda jsou obě hesla stejná (`$form::EQUAL`). A jako parametr dáme odvolávku na první heslo pomocí [hranatých závorek|#Přístup k prvkům]: +Přidáme do formuláře ještě políčko `passwordVerify`, kde uživatel zadá heslo ještě jednou, pro kontrolu. Pomocí validačních pravidel zkontrolujeme, zda jsou obě hesla stejná (`$form::Equal`). A jako parametr dáme odvolávku na první heslo pomocí [hranatých závorek |#Přístup k prvkům]: ```php $form->addPassword('passwordVerify', 'Heslo pro kontrolu:') ->setRequired('Zadejte prosím heslo ještě jednou pro kontrolu') - ->addRule($form::EQUAL, 'Hesla se neshodují', $form['password']) + ->addRule($form::Equal, 'Hesla se neshodují', $form['password']) ->setOmitted(); ``` @@ -226,29 +223,28 @@ Vraťme se k metodě `formSucceeded()`, která ve druhém parametru `$data` dost ```php class RegistrationFormData { - /** @var string */ - public $name; - /** @var int */ - public $age; - /** @var string */ - public $password; + public string $name; + public ?int $age; + public string $password; } ``` -Od PHP 8.0 můžete použít tento elegantní zápis, který využívá konstruktor: +Alternativně můžete využít konstruktor: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` +Property datové třídy mohou být také enumy a dojde k jejich automatickému namapování. .{data-version:3.2.4} + Jak říci Nette, aby nám data vracel jako objekty této třídy? Snadněji než si myslíte. Stačí pouze třídu uvést jako typ parametru `$data` v obslužné metodě: ```php @@ -286,7 +282,7 @@ class PersonFormData class RegistrationFormData { public PersonFormData $person; - public int $age; + public ?int $age; public string $password; } ``` @@ -297,11 +293,13 @@ Mapování pak z typu property `$person` pozná, že má kontejner mapovat na t $person->setMappedType(PersonFormData::class); ``` +Návrh datové třídy formuláře si můžete nechat vygenerovat pomocí metody `Nette\Forms\Blueprint::dataClass($form)`, která ji vypíše do stránky prohlížeče. Kód pak stačí kliknutím označit a zkopírovat do projektu. .{data-version:3.1.15} + Více tlačítek ============= -Pokud má formulář více než jedno tlačítko, potřebujeme zpravidla rozlišit, které z nich bylo stlačeno. Můžeme si pro každé tlačítko vytvořit vlastní obslužnou funkci. Nastavíme ji jako handler pro [událost|nette:glossary#Události] `onClick`: +Pokud má formulář více než jedno tlačítko, potřebujeme zpravidla rozlišit, které z nich bylo stlačeno. Můžeme si pro každé tlačítko vytvořit vlastní obslužnou funkci. Nastavíme ji jako handler pro [událost |nette:glossary#události] `onClick`: ```php $form->addSubmit('save', 'Uložit') @@ -349,7 +347,7 @@ Ochrana před zranitelnostmi Nette Framework klade velký důraz na bezpečnost a proto úzkostlivě dbá na dobré zabezpečení formulářů. Dělá to zcela transparentně a nevyžaduje manuálně nic nastavovat. -Kromě toho, že formuláře ochrání před útokem [Cross Site Scripting (XSS) |nette:glossary#cross-site-scripting-xss] a [Cross-Site Request Forgery (CSRF)|nette:glossary#Cross-Site Request Forgery (CSRF)], dělá spoustu drobných zabezpečení, na které vy už nemusíte myslet. +Kromě toho, že formuláře ochrání před útokem [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] a [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], dělá spoustu drobných zabezpečení, na které vy už nemusíte myslet. Tak třeba odfiltruje ze vstupů všechny kontrolní znaky a prověří validitu UTF-8 kódování, takže data z formuláře budou vždycky čistá. U select boxů a radio listů ověřuje, že vybrané položky byly skutečně z nabízených a nedošlo k podvrhu. Už jsme zmiňovali, že u jednořádkových textových vstupů ostraňuje znaky konce řádků, které tam mohl poslat útočník. U víceřádkových vstupů zase normalizuje znaky pro konce řádků. A tak dále. @@ -367,8 +365,7 @@ Tato ochrana využívá SameSite cookie pojmenovanou `_nss`. Ochrana pomocí Sam $form->addProtection(); ``` -Doporučujeme takto chránit formuláře v administrační části webu, které mění citlivá data v aplikaci. Framework se proti útoku CSRF brání vygenerováním a ověřováním autorizačního tokenu, který se ukládá do session. Proto je nutné před zobrazením formuláře mít otevřenou session. V administrační části webu obvykle už session nastartovaná je kvůli přihlášení uživatele. -Jinak session nastartujte metodou `Nette\Http\Session::start()`. +Doporučujeme takto chránit formuláře v administrační části webu, které mění citlivá data v aplikaci. Framework se proti útoku CSRF brání vygenerováním a ověřováním autorizačního tokenu, který se ukládá do session. Proto je nutné před zobrazením formuláře mít otevřenou session. V administrační části webu obvykle už session nastartovaná je kvůli přihlášení uživatele. Jinak session nastartujte metodou `Nette\Http\Session::start()`. Stejný formulář ve více presenterech @@ -396,9 +393,9 @@ class SignInFormFactory Třídu požádáme o vyrobení formuláře v tovární metodě na komponenty v presenteru: ```php -public function __construct(SignInFormFactory $formFactory) -{ - $this->formFactory = $formFactory; +public function __construct( + private SignInFormFactory $formFactory, +) { } protected function createComponentSignInForm(): Form @@ -432,13 +429,3 @@ class SignInFormFactory ``` Tak, máme za sebou rychlý úvod do formulářů v Nette. Zkuste se ještě podívat do adresáře [examples|https://github.com/nette/forms/tree/master/examples] v distrubuci, kde najdete další inspiraci. - - -/--comment -Občas se může hodit formulář resetovat do stavu před jeho odesláním. To je možné zařídit zavoláním metody `reset()` na odeslaném formuláři: -```php -$form->isSubmitted(); // true -$form->reset(); //formulář je nyní ve stavu, jako by nebyl nikdy odeslán -$form->isSubmitted(); // false -``` -\--- diff --git a/forms/cs/rendering.texy b/forms/cs/rendering.texy index b18fdff37e..e1231ad302 100644 --- a/forms/cs/rendering.texy +++ b/forms/cs/rendering.texy @@ -1,16 +1,18 @@ Vykreslování formulářů ********************** -Vzhled formulářů může být velmi různorodý. V praxi můžeme narazit na dva extrémy. Na jedné straně stojí potřeba v aplikaci vykreslovat řadu formulářů, které jsou si vizuálně podobné jako vejce vejci, a oceníme snadné vykreslení pomocí `$form->render()`. Jde obvykle o případ administračních rozhraní. +Vzhled formulářů může být velmi různorodý. V praxi můžeme narazit na dva extrémy. Na jedné straně stojí potřeba v aplikaci vykreslovat řadu formulářů, které jsou si vizuálně podobné jako vejce vejci, a oceníme snadné vykreslení bez šablony pomocí `$form->render()`. Jde obvykle o případ administračních rozhraní. -Na druhé straně tu jsou rozmanité formuláře, kde platí: co kus, to originál. Jejich podobu nejlépe popíšeme jazykem HTML. A samozřejmě kromě obou zmíněných extrémů narazíme na spoustu formulářů, které se pohybují někde mezi. +Na druhé straně tu jsou rozmanité formuláře, kde platí: co kus, to originál. Jejich podobu nejlépe popíšeme jazykem HTML v šabloně formuláře. A samozřejmě kromě obou zmíněných extrémů narazíme na spoustu formulářů, které se pohybují někde mezi. -Latte -===== +Vykreslení pomocí Latte +======================= [Šablonovací sytém Latte|latte:] zásadně usnadňuje vykreslení formulářů a jejich prvků. Nejprve si ukážeme, jak formuláře vykreslovat ručně po jednotlivých prvcích a tím získat plnou kontrolu nad kódem. Později si ukážeme, jak lze takové vykreslování [zautomatizovat |#Automatické vykreslování]. +Návrh Latte šablony formuláře si můžete nechat vygenerovat pomocí metody `Nette\Forms\Blueprint::latte($form)`, která jej vypíše do stránky prohlížeče. Kód pak stačí kliknutím označit a zkopírovat do projektu. .{data-version:3.1.15} + `{control}` ----------- @@ -21,7 +23,7 @@ Nejjednodušší způsob, jak vykreslit formulář, je napsat v šabloně: {control signInForm} ``` -Ovlivnit podobu takto vykresleného formuláře lze konfigurací [Rendereru|#Renderer] a [jednotlivých prvků|#HTML atributy]. +Ovlivnit podobu takto vykresleného formuláře lze konfigurací [Rendereru |#Renderer] a [jednotlivých prvků |#HTML atributy]. `n:name` @@ -54,8 +56,7 @@ protected function createComponentSignInForm(): Form </form> ``` -Podobu výsledného HTML kódu máte plně ve svých rukou. Pokud atribut `n:name` použijete u elementů `<select>`, `<button>` nebo `<textarea>`, jejich vnitřní obsah se automaticky doplní. -Značka `<form n:name>` navíc vytvoří lokální proměnnou `$form` s objektem kresleného formuláře a uzavírací `</form>` vykreslí všechny nevykreslené hidden prvky (totéž platí i pro `{form} ... {/form}`). +Podobu výsledného HTML kódu máte plně ve svých rukou. Pokud atribut `n:name` použijete u elementů `<select>`, `<button>` nebo `<textarea>`, jejich vnitřní obsah se automaticky doplní. Značka `<form n:name>` navíc vytvoří lokální proměnnou `$form` s objektem kresleného formuláře a uzavírací `</form>` vykreslí všechny nevykreslené hidden prvky (totéž platí i pro `{form} ... {/form}`). Nesmíme ovšem zapomenout na vykreslení možných chybových zpráv. A to jak těch, které se metodou `addError()` přidaly k jednotlivým prvkům (pomocí `{inputError}`), tak i těch přidaných přímo k formuláři (vrací je `$form->getOwnErrors()`): @@ -88,12 +89,6 @@ Složitější formulářové prvky, jako je RadioList nebo CheckboxList, lze ta ``` -Návrh kódu `{formPrint}` .[#toc-formprint] ------------------------------------------- - -Podobný Latte kód pro formulář si můžete nechat vygenerovat pomocí značky `{formPrint}`. Pokud ji umístíte do šablony, místo běžného vykreslení se zobrazí návrh kódu. Ten pak stačí označit a zkopírovat do projektu. - - `{label}` `{input}` ------------------- @@ -135,8 +130,7 @@ Pro vykreslení samotného `<input>` v prvku Checkbox použijte `{input myCheckb `{inputError}` -------------- -Vypíše chybovou zprávu k formulářovému prvku, pokud nějakou má. Zprávu obvykle zabalíme do HTML elementu kvůli stylování. -Předejít vykreslování prázdného elementu, pokud zpráva není, lze elegantně pomocí `n:ifcontent`: +Vypíše chybovou zprávu k formulářovému prvku, pokud nějakou má. Zprávu obvykle zabalíme do HTML elementu kvůli stylování. Předejít vykreslování prázdného elementu, pokud zpráva není, lze elegantně pomocí `n:ifcontent`: ```latte <span class=error n:ifcontent>{inputError $input}</span> @@ -152,11 +146,16 @@ Přítomnost chyby můžeme zjistit metodou `hasErrors()` a podle toho nastavit ``` +`{form}` +-------- + +Značky `{form signInForm}...{/form}` jsou alternativou k `<form n:name="signInForm">...</form>`. + + Automatické vykreslování ------------------------ -Díky značkám `{input}` a `{label}` můžeme snadno vytvořit obecnou šablonu pro jakýkoliv formulář. Bude postupně iterovat a vykreslovat všechny jeho prvky, kromě hidden prvků, které se vykreslí automaticky při ukončení formuláře značkou `</form>`. -Název vykreslovaného formuláře bude očekávat v proměnné `$form`. +Díky značkám `{input}` a `{label}` můžeme snadno vytvořit obecnou šablonu pro jakýkoliv formulář. Bude postupně iterovat a vykreslovat všechny jeho prvky, kromě hidden prvků, které se vykreslí automaticky při ukončení formuláře značkou `</form>`. Název vykreslovaného formuláře bude očekávat v proměnné `$form`. ```latte <form n:name=$form class=form> @@ -181,8 +180,7 @@ Tuto obecnou šablonu si uložte třeba do souboru `basic-form.latte` a pro vykr {include basic-form.latte, form: signInForm} ``` -Pokud byste při vykreslování jednoho určitého formuláře chtěli do jeho podoby zasáhnout a třeba jeden prvek vykreslit jinak, pak je nejjednodušší cestou si v šabloně předpřipravit bloky, které bude možné následně přepsat. -Bloky mohou mít také [dynamické názvy |latte:template-inheritance#dynamicke-nazvy-bloku], lze do nich tak vložit i jméno vykreslovaného prvku. Například: +Pokud byste při vykreslování jednoho určitého formuláře chtěli do jeho podoby zasáhnout a třeba jeden prvek vykreslit jinak, pak je nejjednodušší cestou si v šabloně předpřipravit bloky, které bude možné následně přepsat. Bloky mohou mít také [dynamické názvy |latte:template-inheritance#Dynamické názvy bloků], lze do nich tak vložit i jméno vykreslovaného prvku. Například: ```latte ... @@ -191,7 +189,7 @@ Bloky mohou mít také [dynamické názvy |latte:template-inheritance#dynamicke- ... ``` -Pro prvek např. `username` tak vznikne blok `input-username`, který lze snadno přepsat použitím značky [{embed} |latte:template-inheritance#jednotkova-dedicnost]: +Pro prvek např. `username` tak vznikne blok `input-username`, který lze snadno přepsat použitím značky [{embed} |latte:template-inheritance#Jednotková dědičnost]: ```latte {embed basic-form.latte, form: signInForm} @@ -203,7 +201,7 @@ Pro prvek např. `username` tak vznikne blok `input-username`, který lze snadno {/embed} ``` -Alternativně lze celý obsah šablony `basic-form.latte` [definovat |latte:template-inheritance#definice] jako blok, včetně parametru `$form`: +Alternativně lze celý obsah šablony `basic-form.latte` [definovat |latte:template-inheritance#Definice] jako blok, včetně parametru `$form`: ```latte {define basic-form, $form} @@ -231,15 +229,15 @@ Blok přitom stačí importovat na jediném místě a to na začátku šablony l Speciální případy ----------------- -Pokud potřebujete vykreslit jen vnitřní část formuláře bez HTML značek `<form>` & `</form>`, například při AJAXovém požadavku, můžete formulář otevří a uzavřít do `{formContext} … {/formContext}`. Funguje podobně jako `<form n:form>` či `{form}` v logickém smyslu, tady umožní používat ostatní značky pro kreslení prvků formuláře, ale přitom nic nevykreslí. +Pokud potřebujete vykreslit jen vnitřní část formuláře bez HTML značek `<form>`, například při posílání snippetů, skryjte je pomocí atributu `n:tag-if`: ```latte -{formContext signForm} +<form n:name=signInForm n:tag-if=false> <div> <label n:name=username>Username: <input n:name=username></label> {inputError username} </div> -{/formContext} +</form> ``` S vykreslením prvků uvnitř formulářového kontejneru pomůže tag `{formContainer}`. @@ -256,8 +254,8 @@ S vykreslením prvků uvnitř formulářového kontejneru pomůže tag `{formCon ``` -Bez Latte -========= +Vykreslení bez Latte +==================== Nejjednodušší způsob, jak vykreslit formulář, je zavolat: @@ -265,7 +263,7 @@ Nejjednodušší způsob, jak vykreslit formulář, je zavolat: $form->render(); ``` -Ovlivnit podobu takto vykresleného formuláře lze konfigurací [Rendereru|#Renderer] a [jednotlivých prvků|#HTML atributy]. +Ovlivnit podobu takto vykresleného formuláře lze konfigurací [Rendereru |#Renderer] a [jednotlivých prvků |#HTML atributy]. Manuální vykreslení @@ -299,8 +297,7 @@ Formulář tak lze vykreslovat po jednotlivých elementech: <?php $form->render('end') ?> ``` -Zatímco u některých prvků vrací `getControl()` jediný HTML element (např. `<input>`, `<select>` apod.), u jiných celý kus HTML kódu (CheckboxList, RadioList). -V takovém případě můžete využít metod, které generují jednotlivé inputy a popisky, pro každou položku zvlášt: +Zatímco u některých prvků vrací `getControl()` jediný HTML element (např. `<input>`, `<select>` apod.), u jiných celý kus HTML kódu (CheckboxList, RadioList). V takovém případě můžete využít metod, které generují jednotlivé inputy a popisky, pro každou položku zvlášt: - `getControlPart($key = null): ?Html` vrací HTML kód jedné položky - `getLabelPart($key = null): ?Html` vrací HTML kód popisky jedené položky @@ -321,13 +318,13 @@ Pokud nenastavíme vlastní renderer, bude použit výchozí vykreslovač [api:N <tr class="required"> <th><label class="required" for="frm-name">Jméno:</label></th> - <td><input type="text" class="text" name="name" id="frm-name" value=""></td> + <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> </tr> <tr class="required"> <th><label class="required" for="frm-age">Věk:</label></th> - <td><input type="text" class="text" name="age" id="frm-age" value=""></td> + <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> </tr> <tr> @@ -357,12 +354,12 @@ Výsledkem je tento HTML kód: <dl> <dt><label class="required" for="frm-name">Jméno:</label></dt> - <dd><input type="text" class="text" name="name" id="frm-name" value=""></dd> + <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> <dt><label class="required" for="frm-age">Věk:</label></dt> - <dd><input type="text" class="text" name="age" id="frm-age" value=""></dd> + <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> <dt><label>Pohlaví:</label></dt> @@ -428,6 +425,8 @@ $form->addText('city', 'City:'); $form->addSelect('country', 'Country:', $countries); ``` +Renderer nejprve vykresluje skupiny a teprve poté prvky, které do žádné skupiny nepatří. + Podpora pro Bootstrap --------------------- @@ -438,7 +437,7 @@ Podpora pro Bootstrap HTML atributy ============= -Formulářovým prvkům můžeme nastavovat libovolné HTML atributy pomocí `setHtmlAttribute(string $name, $value = true)`: +Pro nastavení libovolných HTML atributů formulářových prvků použijeme metodu `setHtmlAttribute(string $name, $value = true)`: ```php $form->addInteger('number', 'Číslo:') @@ -448,11 +447,11 @@ $form->addSelect('rank', 'Řazení dle:', ['ceny', 'názvu']) ->setHtmlAttribute('onchange', 'submit()'); // při změně odeslat -// chceme-li to samé udělat pro <form> +// Pro nastavení atributů samotného <form> $form->setHtmlAttribute('id', 'myForm'); ``` -Nastavení typu: +Specifikace typu prvku: ```php $form->addText('tel', 'Váš telefon:') @@ -460,8 +459,10 @@ $form->addText('tel', 'Váš telefon:') ->setHtmlAttribute('placeholder', 'napište telefon'); ``` -Jednotlivým položkám v radio nebo checkbox listech můžeme nastavit HTML atribut s rozdílnými hodnotami pro každou z nich. -Povšimněte si dvojtečky za `style:`, která zajistí volbu hodnoty podle klíče: +.[warning] +Nastavení typu a dalších atributů slouží jen pro vizuální účely. Ověření správnosti vstupů musí probíhat na serveru, což zajístíte volbou vhodného [formulářového prvku|controls] a uvedením [validačních pravidel|validation]. + +Jednotlivým položkám v radio nebo checkbox listech můžeme nastavit HTML atribut s rozdílnými hodnotami pro každou z nich. Povšimněte si dvojtečky za `style:`, která zajistí volbu hodnoty podle klíče: ```php $colors = ['r' => 'červená', 'g' => 'zelená', 'b' => 'modrá']; @@ -478,10 +479,9 @@ Vypíše: <label><input type="checkbox" name="colors[]" value="b">modrá</label> ``` -Pokud jde o logický HTML atribut (který nemá hodnotu, např. `readonly`), můžeme použít zápis s otazníkem: +Pro nastavení logických atributů, jako je `readonly`, můžeme použít zápis s otazníkem: ```php -$colors = ['r' => 'červená', 'g' => 'zelená', 'b' => 'modrá']; $form->addCheckboxList('colors', 'Barvy:', $colors) ->setHtmlAttribute('readonly?', 'r'); // pro více klíču použijte pole, např. ['r', 'g'] ``` @@ -494,8 +494,7 @@ Vypíše: <label><input type="checkbox" name="colors[]" value="b">modrá</label> ``` -V případě selectboxů metoda `setHtmlAttribute()` nastavuje atributy elementu `<select>`. Pokud chceme nastavit atributy jednotlivým -`<option>`, použijeme metodu `setOptionAttribute()`. Fungují i zápisy s dvojtečkou a otazníkem uvedené výše: +V případě selectboxů metoda `setHtmlAttribute()` nastavuje atributy elementu `<select>`. Pokud chceme nastavit atributy jednotlivým `<option>`, použijeme metodu `setOptionAttribute()`. Fungují i zápisy s dvojtečkou a otazníkem uvedené výše: ```php $form->addSelect('colors', 'Barvy:', $colors) @@ -535,9 +534,6 @@ U prvků Checkbox, CheckboxList a RadioList můžete ovlivnit předlohu elementu ```php $input = $form->addCheckbox('send'); -echo $input->getControl(); -// <label><input type="checkbox" name="send"></label> - $html = $input->getContainerPrototype(); $html->setName('div'); // <div> $html->class('check'); // <div class="check"> @@ -545,8 +541,43 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -V připadě CheckboxList a RadioList lze ovlivnit i předlohu oddělovače jednotlivých položek, který vrací metoda `getSeparatorPrototype()`. Ve výchozím stavu je to element `<br>`. Pokud jej změníte na párový element, bude jednotlivé položky obalovat místo oddělovat. -A dále lze ovlivnit předlohu HTML elementu popisky u jednotlivých položek, který vrací `getItemLabelPrototype()`. +V připadě CheckboxList a RadioList lze ovlivnit i předlohu oddělovače jednotlivých položek, který vrací metoda `getSeparatorPrototype()`. Ve výchozím stavu je to element `<br>`. Pokud jej změníte na párový element, bude jednotlivé položky obalovat místo oddělovat. A dále lze ovlivnit předlohu HTML elementu popisky u jednotlivých položek, který vrací `getItemLabelPrototype()`. + + +Překládání +========== + +Pokud programujete vícejazyčnou aplikaci, budete nejspíš potřebovat formulář vykreslit v různých jazykových mutacích. Nette Framework k tomuto účelu definuje rozhraní pro překlad [api:Nette\Localization\Translator]. V Nette není žádná výchozí implementace, můžete si vybrat podle svých potřeb z několika hotových řešeních, které najdete na [Componette |https://componette.org/search/localization]. V jejich dokumentaci se dozvíte, jak translator konfigurovat. + +Formuláře podporují vypisování textů přes translator. Předáme jim ho pomocí metody `setTranslator()`: + +```php +$form->setTranslator($translator); +``` + +Od této chvíle se nejen všechny popisky, ale i všechny chybové hlášky nebo položky select boxů přeloží do jiného jazyka. + +U jednotlivých formulářových prvků je přitom možné nastavit jiný překladač nebo překládání úplně vypnout hodnotou `null`: + +```php +$form->addSelect('carModel', 'Model:', $cars) + ->setTranslator(null); +``` + +U [validačních pravidel|validation] se translatoru předávají i specifické parametry, například u pravidla: + +```php +$form->addPassword('password', 'Heslo:') + ->addRule($form::MinLength, 'Heslo musí mít alespoň %d znaků', 8); +``` + +se volá translator s těmito parametry: + +```php +$translator->translate('Heslo musí mít alespoň %d znaků', 8); +``` + +a tedy může zvolit správný tvar plurálu u slova `znaky` podle počtu. Událost onRender diff --git a/forms/cs/standalone.texy b/forms/cs/standalone.texy index e532fdc7ff..1b980ceeda 100644 --- a/forms/cs/standalone.texy +++ b/forms/cs/standalone.texy @@ -45,7 +45,7 @@ if ($form->isSuccess()) { } ``` -Metoda `getValues()` vrací odeslaná data v podobě objektu [ArrayHash |utils:arrays#ArrayHash]. Jak to změnit si ukážeme [později|#Mapování na třídy]. Objekt `$data` obsahuje klíče `name` a `password` s údaji, které vyplnil uživatel. +Metoda `getValues()` vrací odeslaná data v podobě objektu [ArrayHash |utils:arrays#ArrayHash]. Jak to změnit si ukážeme [později |#Mapování na třídy]. Objekt `$data` obsahuje klíče `name` a `password` s údaji, které vyplnil uživatel. Obvykle data rovnou posíláme k dalšímu zpracování, což může být například vložení do databáze. Během zpracování se ale může objevit chyba, například uživatelské jméno už je obsazené. V takovém případě chybu předáme zpět do formuláře pomocí `addError()` a necháme jej vykreslit znovu, i s chybovou hláškou. @@ -62,7 +62,7 @@ $form->setAction('/submit.php'); $form->setMethod('GET'); ``` -A to je vlastně vše :-) Máme funkční a perfektně [zabezpečený|#Ochrana před zranitelnostmi] formulář. +A to je vlastně vše :-) Máme funkční a perfektně [zabezpečený |#Ochrana před zranitelnostmi] formulář. Zkuste si přidat i další [formulářové prvky|controls]. @@ -103,11 +103,10 @@ Zkuste si odeslat formulář bez vyplněného jména a uvidíte, že se zobrazí Zároveň systém neošidíte tím, že do políčka napíšete třeba jen mezery. Kdepak. Nette levo- i pravostranné mezery automaticky odstraňuje. Vyzkoušejte si to. Je to věc, kterou byste měli s každým jednořádkovým inputem vždy udělat, ale často se na to zapomíná. Nette to dělá automaticky. (Můžete zkusit ošálit formulář a jako jméno poslat víceřádkový řetězec. Ani tady se Nette nenechá zmást a odřádkování změní na mezery.) -Formulář se vždy validuje na straně serveru, ale také se generuje JavaScriptová validace, která proběhne bleskově a uživatel se o chybě dozví okamžitě, bez nutnosti formulář odesílat na server. Tohle má na starosti skript `netteForms.js`. -Vložte jej do stránky: +Formulář se vždy validuje na straně serveru, ale také se generuje JavaScriptová validace, která proběhne bleskově a uživatel se o chybě dozví okamžitě, bez nutnosti formulář odesílat na server. Tohle má na starosti skript `netteForms.js`. Vložte jej do stránky: ```latte -<script src="https://nette.github.io/resources/js/3/netteForms.min.js"></script> +<script src="https://unpkg.com/nette-forms@3"></script> ``` Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si všimnout, že Nette povinné prvky vkládá do elementů s CSS třídou `required`. Zkuste přidat do šablony následující stylopis a popiska „Jméno“ bude červená. Elegantně tak uživatelům vyznačíme povinné prvky: @@ -120,36 +119,36 @@ Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si v Další validační pravidla přidáme metodou `addRule()`. První parametr je pravidlo, druhý je opět text chybové hlášky a může ještě následovat argument validačního pravidla. Co se tím myslí? -Formulář rozšíříme o nové nepovinné políčko „věk“, které musí být celé číslo (`addInteger()`) a navíc v povoleném rozsahu (`$form::RANGE`). A zde právě využijeme třetí parametr metody `addRule()`, kterým předáme validátoru požadovaný rozsah jako dvojici `[od, do]`: +Formulář rozšíříme o nové nepovinné políčko „věk“, které musí být celé číslo (`addInteger()`) a navíc v povoleném rozsahu (`$form::Range`). A zde právě využijeme třetí parametr metody `addRule()`, kterým předáme validátoru požadovaný rozsah jako dvojici `[od, do]`: ```php $form->addInteger('age', 'Věk:') - ->addRule($form::RANGE, 'Věk musí být od 18 do 120', [18, 120]); + ->addRule($form::Range, 'Věk musí být od 18 do 120', [18, 120]); ``` .[tip] Pokud uživatel políčko nevyplní, nebudou se validační pravidla ověřovat, neboť prvek je nepovinný. -Zde vzniká prostor pro drobný refactoring. V chybové hlášce a ve třetím parametru jsou čísla uvedená duplicitně, což není ideální. Pokud bychom tvořili [vícejazyčné formuláře |best-practices:translations] a hláška obsahující čísla by byla přeložena do více jazyků, ztížila by se případná změna hodnot. Z toho důvodu je možné použít zástupné znaky `%d` a Nette hodnoty doplní: +Zde vzniká prostor pro drobný refactoring. V chybové hlášce a ve třetím parametru jsou čísla uvedená duplicitně, což není ideální. Pokud bychom tvořili [vícejazyčné formuláře |rendering#Překládání] a hláška obsahující čísla by byla přeložena do více jazyků, ztížila by se případná změna hodnot. Z toho důvodu je možné použít zástupné znaky `%d` a Nette hodnoty doplní: ```php - ->addRule($form::RANGE, 'Věk musí být od %d do %d let', [18, 120]); + ->addRule($form::Range, 'Věk musí být od %d do %d let', [18, 120]); ``` -Vraťme se k prvku `password`, který taktéž učiníme povinným a ještě ověříme minimální délku hesla (`$form::MIN_LENGTH`), opět s využitím zástupného znaku: +Vraťme se k prvku `password`, který taktéž učiníme povinným a ještě ověříme minimální délku hesla (`$form::MinLength`), opět s využitím zástupného znaku: ```php $form->addPassword('password', 'Heslo:') ->setRequired('Zvolte si heslo') - ->addRule($form::MIN_LENGTH, 'Heslo musí mít alespoň %d znaků', 8); + ->addRule($form::MinLength, 'Heslo musí mít alespoň %d znaků', 8); ``` -Přidáme do formuláře ještě políčko `passwordVerify`, kde uživatel zadá heslo ještě jednou, pro kontrolu. Pomocí validačních pravidel zkontrolujeme, zda jsou obě hesla stejná (`$form::EQUAL`). A jako parametr dáme odvolávku na první heslo pomocí [hranatých závorek|#Přístup k prvkům]: +Přidáme do formuláře ještě políčko `passwordVerify`, kde uživatel zadá heslo ještě jednou, pro kontrolu. Pomocí validačních pravidel zkontrolujeme, zda jsou obě hesla stejná (`$form::Equal`). A jako parametr dáme odvolávku na první heslo pomocí [hranatých závorek |#Přístup k prvkům]: ```php $form->addPassword('passwordVerify', 'Heslo pro kontrolu:') ->setRequired('Zadejte prosím heslo ještě jednou pro kontrolu') - ->addRule($form::EQUAL, 'Hesla se neshodují', $form['password']) + ->addRule($form::Equal, 'Hesla se neshodují', $form['password']) ->setOmitted(); ``` @@ -201,29 +200,28 @@ Vraťme se ke zpracování formulářových dat. Metoda `getValues()` nám vrace ```php class RegistrationFormData { - /** @var string */ - public $name; - /** @var int */ - public $age; - /** @var string */ - public $password; + public string $name; + public ?int $age; + public string $password; } ``` -Od PHP 8.0 můžete použít tento elegantní zápis, který využívá konstruktor: +Alternativně můžete využít konstruktor: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` +Property datové třídy mohou být také enumy a dojde k jejich automatickému namapování. .{data-version:3.2.4} + Jak říci Nette, aby nám data vracel jako objekty této třídy? Snadněji než si myslíte. Stačí pouze název třídy nebo objekt k hydrataci uvést jako parametr: ```php @@ -250,7 +248,7 @@ class PersonFormData class RegistrationFormData { public PersonFormData $person; - public int $age; + public ?int $age; public string $password; } ``` @@ -261,6 +259,8 @@ Mapování pak z typu property `$person` pozná, že má kontejner mapovat na t $person->setMappedType(PersonFormData::class); ``` +Návrh datové třídy formuláře si můžete nechat vygenerovat pomocí metody `Nette\Forms\Blueprint::dataClass($form)`, která ji vypíše do stránky prohlížeče. Kód pak stačí kliknutím označit a zkopírovat do projektu. .{data-version:3.1.15} + Více tlačítek ============= @@ -292,7 +292,7 @@ Ochrana před zranitelnostmi Nette Framework klade velký důraz na bezpečnost a proto úzkostlivě dbá na dobré zabezpečení formulářů. -Kromě toho, že formuláře ochrání před útokem [Cross Site Scripting (XSS) |nette:glossary#cross-site-scripting-xss] a [Cross-Site Request Forgery (CSRF)|nette:glossary#Cross-Site Request Forgery (CSRF)], dělá spoustu drobných zabezpečení, na které vy už nemusíte myslet. +Kromě toho, že formuláře ochrání před útokem [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] a [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], dělá spoustu drobných zabezpečení, na které vy už nemusíte myslet. Tak třeba odfiltruje ze vstupů všechny kontrolní znaky a prověří validitu UTF-8 kódování, takže data z formuláře budou vždycky čistá. U select boxů a radio listů ověřuje, že vybrané položky byly skutečně z nabízených a nedošlo k podvrhu. Už jsme zmiňovali, že u jednořádkových textových vstupů ostraňuje znaky konce řádků, které tam mohl poslat útočník. U víceřádkových vstupů zase normalizuje znaky pro konce řádků. A tak dále. @@ -312,7 +312,6 @@ Ochrana pomocí SameSite cookie nemusí být 100% spolehlivá, proto je vhodné $form->addProtection(); ``` -Doporučujeme takto chránit formuláře v administrační části webu, které mění citlivá data v aplikaci. Framework se proti útoku CSRF brání vygenerováním a ověřováním autorizačního tokenu, který se ukládá do session. Proto je nutné před zobrazením formuláře mít otevřenou session. V administrační části webu obvykle už session nastartovaná je kvůli přihlášení uživatele. -Jinak session nastartujte metodou `Nette\Http\Session::start()`. +Doporučujeme takto chránit formuláře v administrační části webu, které mění citlivá data v aplikaci. Framework se proti útoku CSRF brání vygenerováním a ověřováním autorizačního tokenu, který se ukládá do session. Proto je nutné před zobrazením formuláře mít otevřenou session. V administrační části webu obvykle už session nastartovaná je kvůli přihlášení uživatele. Jinak session nastartujte metodou `Nette\Http\Session::start()`. Tak, máme za sebou rychlý úvod do formulářů v Nette. Zkuste se ještě podívat do adresáře [examples|https://github.com/nette/forms/tree/master/examples] v distrubuci, kde najdete další inspiraci. diff --git a/forms/cs/validation.texy b/forms/cs/validation.texy index e674d5f80f..e313be57b9 100644 --- a/forms/cs/validation.texy +++ b/forms/cs/validation.texy @@ -20,66 +20,77 @@ Validační pravidla přidáváme prvkům metodou `addRule()`. První parametr j ```php $form->addPassword('password', 'Heslo:') - ->addRule($form::MIN_LENGTH, 'Heslo musí mít alespoň %d znaků', 8); + ->addRule($form::MinLength, 'Heslo musí mít alespoň %d znaků', 8); ``` -Nette přichází s celou řadu předdefinovaných pravidel, jejichž názvy jsou konstanty třídy `Nette\Forms\Form`. +**Validační pravidla se ověřují pouze v případě, že uživatel prvek vyplnil.** -U všech prvků můžeme použít tyto pravidla: +Nette přichází s celou řadu předdefinovaných pravidel, jejichž názvy jsou konstanty třídy `Nette\Forms\Form`. U všech prvků můžeme použít tyto pravidla: | konstanta | popis | typ argumentu |------- -| `REQUIRED` | povinný prvek, alias pro `setRequired()` | - -| `FILLED` | povinný prvek, alias pro `setRequired()` | - -| `BLANK` | prvek nesmí být vyplněn | - -| `EQUAL` | hodnota se rovná parametru | `mixed` -| `NOT_EQUAL` | hodnota se nerovná parametru | `mixed` -| `IS_IN` | hodnota se rovná některé položce v poli | `array` -| `IS_NOT_IN` | hodnota se nerovná žádné položce v poli | `array` -| `VALID` | je prvek vyplněn správně? (pro [#podmínky]) | - - -U prvků `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` lze použít i následující pravidla: - -| `MIN_LENGTH` | minimální délka textu | `int` -| `MAX_LENGTH` | maximální délka textu | `int` -| `LENGTH` | délka v rozsahu nebo přesná délka | dvojice `[int, int]` nebo `int` -| `EMAIL` | platná e-mailová adresa | - +| `Required` | povinný prvek, alias pro `setRequired()` | - +| `Filled` | povinný prvek, alias pro `setRequired()` | - +| `Blank` | prvek nesmí být vyplněn | - +| `Equal` | hodnota se rovná parametru | `mixed` +| `NotEqual` | hodnota se nerovná parametru | `mixed` +| `IsIn` | hodnota se rovná některé položce v poli | `array` +| `IsNotIn` | hodnota se nerovná žádné položce v poli | `array` +| `Valid` | je prvek vyplněn správně? (pro [#podmínky]) | - + + +Textové vstupy +-------------- + +U prvků `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` lze použít i některá následující pravidla: + +| `MinLength` | minimální délka textu | `int` +| `MaxLength` | maximální délka textu | `int` +| `Length` | délka v rozsahu nebo přesná délka | dvojice `[int, int]` nebo `int` +| `Email` | platná e-mailová adresa | - | `URL` | absolutní URL | - -| `PATTERN` | vyhovuje regulárnímu výrazu | `string` -| `PATTERN_ICASE` | jako `PATTERN`, ale nezávislé na velikosti písmen | `string` -| `INTEGER` | celočíselná hodnota | - -| `NUMERIC` | alias pro `INTEGER` | - -| `FLOAT` | číslo | - -| `MIN` | minimální hodnota číselného prvku | `int\|float` -| `MAX` | maximální hodnota číselného prvku | `int\|float` -| `RANGE` | hodnota v rozsahu | dvojice `[int\|float, int\|float]` - -Validační pravidla `INTEGER`, `NUMERIC` a `FLOAT` rovnou převádí hodnotu na integer resp. float. A dále pravidlo `URL` akceptuje i adresu bez schématu (např. `nette.org`) a schéma doplní (`https://nette.org`). -Výraz v `PATTERN` a `PATTERN_ICASE` musí platit pro celou hodnotu, tj. jako by byl obalen znaky `^` a `$`. +| `Pattern` | vyhovuje regulárnímu výrazu | `string` +| `PatternInsensitive` | jako `Pattern`, ale nezávislé na velikosti písmen | `string` +| `Integer` | celočíselná hodnota | - +| `Numeric` | alias pro `Integer` | - +| `Float` | číslo | - +| `Min` | minimální hodnota číselného prvku | `int\|float` +| `Max` | maximální hodnota číselného prvku | `int\|float` +| `Range` | hodnota v rozsahu | dvojice `[int\|float, int\|float]` -U prvků `addUpload()`, `addMultiUpload()` lze použít i následující pravidla: +Validační pravidla `Integer`, `Numeric` a `Float` rovnou převádí hodnotu na integer resp. float. A dále pravidlo `URL` akceptuje i adresu bez schématu (např. `nette.org`) a schéma doplní (`https://nette.org`). Výraz v `Pattern` a `PatternIcase` musí platit pro celou hodnotu, tj. jako by byl obalen znaky `^` a `$`. -| `MAX_FILE_SIZE` | maximální velikost souboru | `int` -| `MIME_TYPE` | MIME type, povoleny zástupné znaky (`'video/*'`) | `string\|string[]` -| `IMAGE` | obrázek JPEG, PNG, GIF, WebP | - -| `PATTERN` | jméno souboru vyhovuje regulárnímu výrazu | `string` -| `PATTERN_ICASE` | jako `PATTERN`, ale nezávislé na velikosti písmen | `string` -`MIME_TYPE` a `IMAGE` vyžadují PHP rozšíření `fileinfo`. Že je soubor či obrázek požadovaného typu detekují na základě jeho signatury a **neověřují integritu celého souboru.** Zda není obrázek poškozený lze zjistit například pokusem o jeho [načtení|http:request#toImage]. +Počet položek +------------- U prvků `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` lze použít i následující pravidla pro omezení počtu vybraných položek resp. uploadovaných souborů: -| `MIN_LENGTH` | minimální počet | `int` -| `MAX_LENGTH` | maximální počet | `int` -| `LENGTH` | počet v rozsahu nebo přesný počet | dvojice `[int, int]` nebo `int` +| `MinLength` | minimální počet | `int` +| `MaxLength` | maximální počet | `int` +| `Length` | počet v rozsahu nebo přesný počet | dvojice `[int, int]` nebo `int` -Chybové hlášky +Upload souborů -------------- -Všechny předdefinované pravidla s výjimkou `PATTERN` a `PATTERN_ICASE` mají výchozí chybovou hlášku, takže ji lze vynechat. Nicméně uvedením a formulací všech hlášek na míru uděláte formulář uživatelsky přívětivější. +U prvků `addUpload()`, `addMultiUpload()` lze použít i následující pravidla: -Změnit výchozí hlášky můžete v [konfiguraci|forms:configuration], úpravou textů v poli `Nette\Forms\Validator::$messages` nebo použitím [translatoru|best-practices:translations#Překlad formulářů]. +| `MaxFileSize` | maximální velikost souboru v bajtech | `int` +| `MimeType` | MIME type, povoleny zástupné znaky (`'video/*'`) | `string\|string[]` +| `Image` | obrázek JPEG, PNG, GIF, WebP, AVIF | - +| `Pattern` | jméno souboru vyhovuje regulárnímu výrazu | `string` +| `PatternInsensitive` | jako `Pattern`, ale nezávislé na velikosti písmen | `string` + +`MimeType` a `Image` vyžadují PHP rozšíření `fileinfo`. Že je soubor či obrázek požadovaného typu detekují na základě jeho signatury a **neověřují integritu celého souboru.** Zda není obrázek poškozený lze zjistit například pokusem o jeho [načtení |http:request#toImage]. + + +Chybové hlášky +============== + +Všechny předdefinované pravidla s výjimkou `Pattern` a `PatternInsensitive` mají výchozí chybovou hlášku, takže ji lze vynechat. Nicméně uvedením a formulací všech hlášek na míru uděláte formulář uživatelsky přívětivější. + +Změnit výchozí hlášky můžete v [konfiguraci|forms:configuration], úpravou textů v poli `Nette\Forms\Validator::$messages` nebo použitím [translatoru |rendering#Překládání]. V textu chybových hlášek lze používat tyto zástupné řetězce: @@ -94,10 +105,10 @@ $form->addText('name', 'Jméno:') ->setRequired('Vyplňte prosím %label'); $form->addInteger('id', 'ID:') - ->addRule($form::RANGE, 'nejméně %d a nejvíce %d', [5, 10]); + ->addRule($form::Range, 'nejméně %d a nejvíce %d', [5, 10]); $form->addInteger('id', 'ID:') - ->addRule($form::RANGE, 'nejvíce %2$d a nejméně %1$d', [5, 10]); + ->addRule($form::Range, 'nejvíce %2$d a nejméně %1$d', [5, 10]); ``` @@ -109,9 +120,9 @@ Kromě pravidel lze přidávat také podmínky. Ty se zapisují podobně jako pr ```php $form->addPassword('password', 'Heslo:') // pokud není heslo delší než 8 znaků - ->addCondition($form::MAX_LENGTH, 8) + ->addCondition($form::MaxLength, 8) // pak musí obsahovat číslici - ->addRule($form::PATTERN, 'Musí obsahovat číslici', '.*[0-9].*'); + ->addRule($form::Pattern, 'Musí obsahovat číslici', '.*[0-9].*'); ``` Podmínku je možné vázat i na jiný prvek než aktuální pomocí `addConditionOn()`. Jako první parametr uvedeme referenci na prvek. V této ukázce bude e-mail povinný jen tehdy, zaškrtne-li se checkbox (jeho hodnota bude true): @@ -121,7 +132,7 @@ $form->addCheckbox('newsletters', 'zasílejte mi newslettery'); $form->addEmail('email', 'E-mail:') // pokud je checkbox zaškrtnut - ->addConditionOn($form['newsletters'], $form::EQUAL, true) + ->addConditionOn($form['newsletters'], $form::Equal, true) // pak vyžaduj e-mail ->setRequired('Zadejte e-mailovou adresu'); ``` @@ -140,18 +151,18 @@ $form->addText(/* ... */) ->addRule(/* ... */); ``` -V Nette lze velmi snadno reagovat na splnění či nesplnění podmíny i na straně JavaScriptu pomocí metody `toggle()`, viz [#dynamický JavaScript]. +V Nette lze velmi snadno reagovat na splnění či nesplnění podmínky i na straně JavaScriptu pomocí metody `toggle()`, viz [#dynamický JavaScript]. -Reference mezi prvky -==================== +Reference na jiný prvek +======================= -Jako argument pravidla či podmínky lze uvádět referenci na jiný prvek. Takto lze např. dynamicky validovat, že prvek `text` má tolik znaků, jako je hodnota prvku `length`: +Jako argument pravidla či podmínky lze předat i jiný prvek formuláře. Pravidlo potom použije hodnotu vloženou později uživatelem v prohlížeči. Takto lze např. dynamicky validovat, že prvek `password` obsahuje stejný řetězec jako prvek `password_confirm`: ```php -$form->addInteger('length'); -$form->addText('text') - ->addRule($form::LENGTH, null, $form['length']); +$form->addPassword('password', 'Heslo'); +$form->addPassword('password_confirm', 'Potvrďte heslo') + ->addRule($form::Equal, 'Zadaná hesla se neshodují', $form['password']); ``` @@ -160,7 +171,7 @@ Vlastní pravidla a podmínky Občas se dostaneme do situace, kdy nám vestavěná validační pravidla v Nette nestačí a potřebujeme data od uživatele validovat po svém. V Nette je to velmi jednoduché! -Metodám `addRule()` či `addCondition()` lze první parametr předat libovolný callback. Ten přijímá jako první parametr samotný prvek a vrací boolean hodnotu určující, zda validace proběhla v pořádku. Při přidávání pravidla pomocí `addRule()` je možné zadat i další argumenty, ty jsou pak předány jako druhý parametr. +Metodám `addRule()` či `addCondition()` lze jako první parametr předat libovolný callback. Ten přijímá jako první parametr samotný prvek a vrací boolean hodnotu určující, zda validace proběhla v pořádku. Při přidávání pravidla pomocí `addRule()` je možné zadat i další argumenty, ty jsou pak předány jako druhý parametr. Vlastní sadu validátorů tak můžeme vytvořit jako třídu se statickými metodami: @@ -187,7 +198,7 @@ $form->addInteger('num') ->addRule( [MyValidators::class, 'validateDivisibility'], 'Hodnota musí být násobkem čísla %d', - 8 + 8, ); ``` @@ -203,7 +214,7 @@ Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => Událost onValidate ================== -Po odeslání formuláře se provádí validace, kdy se zkontrolují jednotlivá pravidla přidaná pomocí `addRule()` a následně se vyvolá [událost|nette:glossary#Události] `onValidate`. Její handler lze využít k doplňkové validaci, typicky ověření správné kombinace hodnot ve více prvcích formuláře. +Po odeslání formuláře se provádí validace, kdy se zkontrolují jednotlivá pravidla přidaná pomocí `addRule()` a následně se vyvolá [událost |nette:glossary#události] `onValidate`. Její handler lze využít k doplňkové validaci, typicky ověření správné kombinace hodnot ve více prvcích formuláře. Pokud se odhalí chyba, předáme ji do formuláře metodou `addError()`. Tu lze volat buď na konkrétním prvku, nebo přímo na formuláři. @@ -234,10 +245,10 @@ V mnoha případech se o chybě dozvíme až ve chvíli, kdy zpracováváme plat try { $data = $form->getValues(); $this->user->login($data->username, $data->password); - $this->redirect('Homepage:'); + $this->redirect('Home:'); } catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::INVALID_CREDENTIAL) { + if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { $form->addError('Neplatné heslo.'); } } @@ -264,7 +275,7 @@ $form->addText('zip', 'PSČ:') ->addFilter(function ($value) { return str_replace(' ', '', $value); // odstraníme mezery z PSČ }) - ->addRule($form::PATTERN, 'PSČ není ve tvaru pěti číslic', '\d{5}'); + ->addRule($form::Pattern, 'PSČ není ve tvaru pěti číslic', '\d{5}'); ``` Filtr se začlení mezi validační pravidla a podmínky a tedy záleží na pořadí metod, tj. filtr a pravidlo se volají v takovém pořadí, jako je pořadí metod `addFilter()` a `addRule()`. @@ -273,15 +284,14 @@ Filtr se začlení mezi validační pravidla a podmínky a tedy záleží na po JavaScriptová validace ====================== -Jazyk pro formulování podmínek a pravidel je velice silný. Všechny konstrukce přitom fungují jak na straně serveru, tak i na straně JavaScriptu. Přenáší se v HTML atributech `data-nette-rules` jako JSON. -Samotnou validaci pak provádí skript, který odchytí událost formuláře `submit`, projde jednotlivé prvky a vykoná příslušnou validaci. +Jazyk pro formulování podmínek a pravidel je velice silný. Všechny konstrukce přitom fungují jak na straně serveru, tak i na straně JavaScriptu. Přenáší se v HTML atributech `data-nette-rules` jako JSON. Samotnou validaci pak provádí skript, který odchytí událost formuláře `submit`, projde jednotlivé prvky a vykoná příslušnou validaci. Tím skriptem je `netteForms.js` a je dostupný z více možných zdrojů: Skript můžete vložit přímo do HTML stránky ze CDN: ```latte -<script src="https://nette.github.io/resources/js/3/netteForms.min.js"></script> +<script src="https://unpkg.com/nette-forms@3"></script> ``` Nebo zkopírovat lokálně do veřejné složky projektu (např. z `vendor/nette/forms/src/assets/netteForms.min.js`): @@ -292,7 +302,7 @@ Nebo zkopírovat lokálně do veřejné složky projektu (např. z `vendor/nette Nebo nainstalovat přes [npm|https://www.npmjs.com/package/nette-forms]: -```bash +```shell npm install nette-forms ``` @@ -318,7 +328,7 @@ Chcete zobrazit políčka pro zadání adresy pouze pokud uživatel zvolí zbož ```php $form->addCheckbox('send_it') - ->addCondition($form::EQUAL, true) + ->addCondition($form::Equal, true) ->toggle('#address-container'); ``` @@ -352,7 +362,7 @@ $details->addInteger('age') $details->addInteger('age2') ->setRequired('age2'); -$form->addSubmit('send1'); // Validuje celý formuláře +$form->addSubmit('send1'); // Validuje celý formulář $form->addSubmit('send2') ->setValidationScope([]); // Nevaliduje vůbec $form->addSubmit('send3') diff --git a/forms/en/@home.texy b/forms/en/@home.texy index 2f4c2d1b11..a9b2549490 100644 --- a/forms/en/@home.texy +++ b/forms/en/@home.texy @@ -1,24 +1,24 @@ -Forms -***** +Nette Forms +*********** <div class=perex> -Nette Forms has revolutionized the creation of web forms. All you had to do was write a few clear lines of code and you had a form, including rendering, JavaScript, and server validation, plus industry-leading security. Let's see how +Nette Forms revolutionized the creation of web forms. Suddenly, writing just a few clear lines of code was enough to get a complete form, including rendering, JavaScript and server-side validation, plus top-notch security. We'll show you how to: -- create friendly forms -- validate sent data -- draw elements exactly as needed +- create user-friendly forms +- validate submitted data +- render elements exactly as needed </div> -With Nette Forms, you can diminish routine tasks like writing validation (on both server and client-side), and minimizing the likelihood of errors and security issues. +Using Nette Forms, you can avoid many routine tasks, such as writing validation logic (both server-side and client-side), and minimize the probability of errors and security vulnerabilities. -You can use the forms either as a part of the Nette Application (ie in presenters) or standalone. Because the use is slightly different in both cases, we have prepared separated instructions for you: +You can use forms either as part of a Nette Application (i.e., in presenters) or completely standalone. Since the usage differs slightly in both cases, we have prepared separate guides for you: <div class="wiki-buttons"> -<div> "Forms in presenters .[wiki-button]":in-presenter </div> -<div> "Forms standalone .[wiki-button]":standalone </div> +<div> "Forms in Presenters .[wiki-button]":in-presenter </div> +<div> "Forms Standalone .[wiki-button]":standalone </div> </div> diff --git a/forms/en/@left-menu.texy b/forms/en/@left-menu.texy index a3d71b3d07..f23cd43594 100644 --- a/forms/en/@left-menu.texy +++ b/forms/en/@left-menu.texy @@ -1,5 +1,5 @@ -Forms -***** +Nette Forms +*********** - [Overview |@home] - [Forms in Presenters|in-presenter] - [Forms Standalone|standalone] @@ -9,6 +9,6 @@ Forms - [Configuration] -Further reading +Further Reading *************** - [Best practices |best-practices:] diff --git a/forms/en/@meta.texy b/forms/en/@meta.texy new file mode 100644 index 0000000000..42471908b0 --- /dev/null +++ b/forms/en/@meta.texy @@ -0,0 +1 @@ +{{sitename: Nette Documentation}} diff --git a/forms/en/configuration.texy b/forms/en/configuration.texy index 6c33049a29..ea3c55f991 100644 --- a/forms/en/configuration.texy +++ b/forms/en/configuration.texy @@ -1,5 +1,5 @@ -Configuring Forms -***************** +Forms Configuration +******************* .[perex] You can change the default [form error messages|validation] in the configuration. @@ -7,27 +7,55 @@ You can change the default [form error messages|validation] in the configuration ```neon forms: messages: - EQUAL: 'Please enter %s.' - NOT_EQUAL: 'This value should not be %s.' - FILLED: 'This field is required.' - BLANK: 'This field should be blank.' - MIN_LENGTH: 'Please enter at least %d characters.' - MAX_LENGTH: 'Please enter no more than %d characters.' - LENGTH: 'Please enter a value between %d and %d characters long.' - EMAIL: 'Please enter a valid email address.' + Equal: 'Please enter %s.' + NotEqual: 'This value should not be %s.' + Filled: 'This field is required.' + Blank: 'This field should be blank.' + MinLength: 'Please enter at least %d characters.' + MaxLength: 'Please enter no more than %d characters.' + Length: 'Please enter a value between %d and %d characters long.' + Email: 'Please enter a valid email address.' URL: 'Please enter a valid URL.' - INTEGER: 'Please enter a valid integer.' - FLOAT: 'Please enter a valid number.' - MIN: 'Please enter a value greater than or equal to %d.' - MAX: 'Please enter a value less than or equal to %d.' - RANGE: 'Please enter a value between %d and %d.' - MAX_FILE_SIZE: 'The size of the uploaded file can be up to %d bytes.' - MAX_POST_SIZE: 'The uploaded data exceeds the limit of %d bytes.' - MIME_TYPE: 'The uploaded file is not in the expected format.' - IMAGE: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' - Nette\Forms\Controls\SelectBox::VALID: 'Please select a valid option.' - Nette\Forms\Controls\UploadControl::VALID: 'An error occurred during file upload.' - Nette\Forms\Controls\CsrfProtection::PROTECTION: 'Your session has expired. Please return to the home page and try again.' + Integer: 'Please enter a valid integer.' + Float: 'Please enter a valid number.' + Min: 'Please enter a value greater than or equal to %d.' + Max: 'Please enter a value less than or equal to %d.' + Range: 'Please enter a value between %d and %d.' + MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' + MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' + MimeType: 'The uploaded file is not in the expected format.' + Image: 'The uploaded file must be image in format JPEG, GIF, PNG, WebP or AVIF.' + Nette\Forms\Controls\SelectBox::Valid: 'Please select a valid option.' + Nette\Forms\Controls\UploadControl::Valid: 'An error occurred during file upload.' + Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' ``` +/--comment + + + + + + + + + + + + + + + + + + + + + + + + + +\-- + If you are not using the whole framework and therefore not even the configuration files, you can change the default error messages directly in the `Nette\Forms\Validator::$messages` field. diff --git a/forms/en/controls.texy b/forms/en/controls.texy index e713026246..0850b9ab5e 100644 --- a/forms/en/controls.texy +++ b/forms/en/controls.texy @@ -2,13 +2,13 @@ Form Controls ************* .[perex] -Overview of built-in form controls. +Overview of standard form controls. -addText(string|int $name, $label=null): TextInput .[method] -=========================================================== +addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== -Adds single line text field (class [TextInput |api:Nette\Forms\Controls\TextInput]). If the user does not fill in the field, it returns an empty string `''`, or use `setNullable()` to change it to return `null`. +Adds a single-line text input field (class [TextInput |api:Nette\Forms\Controls\TextInput]). If the user does not fill in the field, it returns an empty string `''`, or use `setNullable()` to make it return `null` instead. ```php $form->addText('name', 'Name:') @@ -16,19 +16,16 @@ $form->addText('name', 'Name:') ->setNullable(); ``` -It automatically validates UTF-8, trims left and right whitespaces, and removes line breaks that could be sent by an attacker. +Automatically validates UTF-8, trims leading and trailing whitespace, and removes line breaks that could be sent by an attacker. -The maximum length can be limited using `setMaxLength()`. The [addFilter()|validation#Modifying Input Values] allows you to change the user-entered value. +The maximum length can be limited using `setMaxLength()`. The [addFilter() |validation#Modifying Input Values] method allows modifying the value entered by the user. -Use `setHtmlType()` to change the [character|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types] of input element to `search`, `tel`, `url`, `range`, `date`, `datetime-local`, `month`, `time`, `week`, `color`. Instead of the `number` and `email` types, we recommend using [#addInteger] and [#addEmail], which provide server-side validation. +Using `setHtmlType()`, you can change the visual appearance of the text field to types like `search`, `tel`, or `url` as defined in the [specification|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Remember that changing the type is purely visual and does not replace validation functionality. For the `url` type, it's advisable to add a specific [URL validation rule |validation#Text inputs]. -```php -$form->addText('color', 'Choose color:') - ->setHtmlType('color') - ->addRule($form::PATTERN, 'invalid value', '[0-9a-f]{6}'); -``` +.[note] +For other input types like `number`, `range`, `email`, `date`, `datetime-local`, `time`, and `color`, use specialized methods such as [#addInteger], [#addFloat], [#addEmail], [#addDate], [#addTime], [#addDateTime], and [#addColor], which provide server-side validation. The types `month` and `week` are not yet fully supported by all browsers. -The so-called empty-value can be set for the element, which is something like the default value, but if the user does not overwrite it, returns empty string or `null`. +An "empty value" can be set for the control. This acts somewhat like a default value, but if the user doesn't change it, the control returns an empty string or `null`. ```php $form->addText('phone', 'Phone:') @@ -40,63 +37,80 @@ $form->addText('phone', 'Phone:') addTextArea(string|int $name, $label=null): TextArea .[method] ============================================================== -Adds a multiline text field (class [TextArea |api:Nette\Forms\Controls\TextArea]). If the user does not fill in the field, it returns an empty string `''`, or use `setNullable()` to change it to return `null`. +Adds a multi-line text input field (class [TextArea |api:Nette\Forms\Controls\TextArea]). If the user doesn't fill in the field, it returns an empty string `''`, or use `setNullable()` to make it return `null` instead. ```php $form->addTextArea('note', 'Note:') - ->addRule($form::MAX_LENGTH, 'Your note is way too long', 10000); + ->addRule($form::MaxLength, 'Your note is way too long', 10000); ``` -Automatically validates UTF-8 and normalizes line breaks to `\n`. Unlike a single-line input field, it does not trim the whitespace. +Automatically validates UTF-8 and normalizes line endings to `\n`. Unlike the single-line input field, no whitespace trimming occurs. -The maximum length can be limited using `setMaxLength()`. The [addFilter()|validation#Modifying Input Values] allows you to change the user-entered value. You can set the so-called empty-value using `setEmptyValue()`. +The maximum length can be limited using `setMaxLength()`. The [addFilter() |validation#Modifying Input Values] method allows modifying the user-entered value. An empty value can be set using `setEmptyValue()`. addInteger(string|int $name, $label=null): TextInput .[method] ============================================================== -Adds input field for integer (class [TextInput |api:Nette\Forms\Controls\TextInput]). Returns either an integer or `null` if the user does not enter anything. +Adds an input field for entering an integer (class [TextInput |api:Nette\Forms\Controls\TextInput]). Returns either an integer or `null` if the user enters nothing. + +```php +$form->addInteger('year', 'Year:') + ->addRule($form::Range, 'The year must be between %d and %d.', [1900, 2023]); +``` + +The control renders as `<input type="number">`. Using the `setHtmlType()` method, you can change the type to `range` for display as a slider, or to `text` if you prefer a standard text field without the special behavior of the `number` type. + + +addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} +================================================================================= + +Adds an input field for entering a floating-point number (class [TextInput |api:Nette\Forms\Controls\TextInput]). Returns either a float or `null` if the user enters nothing. ```php -$form->addInteger('level', 'Level:') - ->setDefaultValue(0) - ->addRule($form::RANGE, 'Level must be between %d and %d.', [0, 100]); +$form->addFloat('level', 'Level:') + ->setDefaultValue(0.0) // Explicitly set float default + ->addRule($form::Range, 'The level must be between %f and %f.', [0.0, 100.0]); // Use float range and %f ``` +The control renders as `<input type="number">`. Using the `setHtmlType()` method, you can change the type to `range` for display as a slider, or to `text` if you prefer a standard text field without the special behavior of the `number` type. + +Nette and the Chrome browser accept both a comma and a dot as decimal separators. To enable this functionality in Firefox as well, it is recommended to set the `lang` attribute either for the specific control or for the entire page, for example, `<html lang="en">`. -addEmail(string|int $name, $label=null): TextInput .[method] -============================================================ -Adds email address field with validity check (class [TextInput |api:Nette\Forms\Controls\TextInput]). If the user does not fill in the field, it returns an empty string `''`, or use `setNullable()` to change it to return `null`. +addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] +================================================================================ + +Adds an input field for entering an email address (class [TextInput |api:Nette\Forms\Controls\TextInput]). If the user doesn't fill in the field, it returns an empty string `''`, or use `setNullable()` to make it return `null` instead. ```php -$form->addEmail('email', 'Email:'); +$form->addEmail('email', 'E-mail:'); ``` -Verifies that the value is a valid email address. It does not verify that the domain actually exists, only the syntax is verified. Automatically validates UTF-8, trims left and right whitespaces. +Validates that the value is a valid email address. It does not check if the domain actually exists, only the syntax is verified. Automatically validates UTF-8 and trims leading and trailing whitespace. -The maximum length can be limited using `setMaxLength()`. The [addFilter()|validation#Modifying Input Values] allows you to change the user-entered value. You can set the so-called empty-value using `setEmptyValue()`. +The maximum length can be limited using `setMaxLength()`. The [addFilter() |validation#Modifying Input Values] method allows modifying the user-entered value. An empty value can be set using `setEmptyValue()`. -addPassword(string|int $name, $label=null): TextInput .[method] -=============================================================== +addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +====================================================================================================== -Adds password field (class [TextInput |api:Nette\Forms\Controls\TextInput]). +Adds a password input field (class [TextInput |api:Nette\Forms\Controls\TextInput]). ```php $form->addPassword('password', 'Password:') ->setRequired() - ->addRule($form::MIN_LENGTH, 'Password has to be at least %d characters long', 8) - ->addRule($form::PATTERN, 'Password must contain a number', '.*[0-9].*'); + ->addRule($form::MinLength, 'Password must be at least %d characters long', 8) + ->addRule($form::Pattern, 'Password must contain a number', '.*[0-9].*'); ``` -When you re-send the form, the input will be blank. It automatically validates UTF-8, trims left and right whitespaces, and removes line breaks that could be sent by an attacker. +When the form is redisplayed, the field will be empty. Automatically validates UTF-8, trims leading and trailing whitespace, and removes line breaks that could be sent by an attacker. addCheckbox(string|int $name, $caption=null): Checkbox .[method] ================================================================ -Adds a checkbox (class [Checkbox |api:Nette\Forms\Controls\Checkbox]). The field returns either `true` or `false`, depending on whether it is checked. +Adds a checkbox (class [Checkbox |api:Nette\Forms\Controls\Checkbox]). Returns `true` or `false`, depending on whether it is checked. ```php $form->addCheckbox('agree', 'I agree with terms') @@ -104,10 +118,10 @@ $form->addCheckbox('agree', 'I agree with terms') ``` -addCheckboxList(string|int $name, $label=null, array $items=null): CheckboxList .[method] -========================================================================================= +addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] +========================================================================================== -Adds list of checkboxes for selecting multiple items (class [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Returns the array of keys of the selected items. The `getSelectedItems()` method returns values instead of keys. +Adds a list of checkboxes for selecting multiple items (class [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Returns an array of the keys of the selected items. The `getSelectedItems()` method returns the values instead of keys. ```php $form->addCheckboxList('colors', 'Colors:', [ @@ -117,19 +131,25 @@ $form->addCheckboxList('colors', 'Colors:', [ ]); ``` -We pass the array of items as the third parameter, or by the `setItems()` method. +Pass the array of offered items as the third parameter or using the `setItems()` method. -You can use `setDisabled(['r', 'g'])` to disable individual items. +Use `setDisabled(['r', 'g'])` to disable individual items. -The element automatically checks that there has been no forgery and that the selected items are actually one of the offered ones and have not been disabled. The `getRawValue()` method can be used to retrieve submitted items without this important check. +The control automatically checks that no forgery occurred and that the selected items are indeed among the offered ones and were not disabled. The `getRawValue()` method can be used to retrieve submitted items without this important check. -When default values are set, it also checks that they are one of the offered items, otherwise it throws an exception. This check can be turned off with `checkDefaultValue(false)`. +When setting default selected items, it also checks that they are among the offered ones, otherwise it throws an exception. This check can be disabled using `checkDefaultValue(false)`. +If you are submitting the form using the `GET` method, you can choose a more compact data transfer method that saves query string size. Activate it by setting an HTML attribute on the form: -addRadioList(string|int $name, $label=null, array $items=null): RadioList .[method] -=================================================================================== +```php +$form->setHtmlAttribute('data-nette-compact'); +``` -Adds radio buttons (class [RadioList |api:Nette\Forms\Controls\RadioList]). Returns the key of the selected item, or `null` if the user did not select anything. The `getSelectedItem()` method returns a value instead of a key. + +addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] +==================================================================================== + +Adds radio buttons (class [RadioList |api:Nette\Forms\Controls\RadioList]). Returns the key of the selected item, or `null` if the user selected nothing. The `getSelectedItem()` method returns the value instead of the key. ```php $sex = [ @@ -139,23 +159,23 @@ $sex = [ $form->addRadioList('gender', 'Gender:', $sex); ``` -We pass the array of items as the third parameter, or by the `setItems()` method. +Pass the array of offered items as the third parameter or using the `setItems()` method. -You can use `setDisabled(['m'])` to disable individual items. +Use `setDisabled(['m'])` to disable individual items. -The element automatically checks that there has been no forgery and that the selected item is actually one of the offered ones and has not been disabled. The `getRawValue()` method can be used to retrieve the submitted item without this important check. +The control automatically checks that no forgery occurred and that the selected item is indeed one of the offered ones and was not disabled. The `getRawValue()` method can be used to retrieve the submitted item without this important check. -When default value is set, it also checks that it is one of the offered items, otherwise it throws an exception. This check can be turned off with `checkDefaultValue(false)`. +When setting the default selected item, it also checks that it is one of the offered ones, otherwise it throws an exception. This check can be disabled using `checkDefaultValue(false)`. -addSelect(string|int $name, $label=null, array $items=null): SelectBox .[method] -================================================================================ +addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +================================================================================================== -Adds select box (class [SelectBox |api:Nette\Forms\Controls\SelectBox]). Returns the key of the selected item, or `null` if the user did not select anything. The `getSelectedItem()` method returns a value instead of a key. +Adds a select box (class [SelectBox |api:Nette\Forms\Controls\SelectBox]). Returns the key of the selected item, or `null` if the user selected nothing. The `getSelectedItem()` method returns the value instead of the key. ```php $countries = [ - 'CZ' => 'Czech republic', + 'CZ' => 'Czech Republic', 'SK' => 'Slovakia', 'GB' => 'United Kingdom', ]; @@ -164,12 +184,12 @@ $form->addSelect('country', 'Country:', $countries) ->setDefaultValue('SK'); ``` -We pass the array of items as the third parameter, or by the `setItems()` method. The array of items can also be two-dimensional: +Pass the array of offered items as the third parameter or using the `setItems()` method. Items can also be a two-dimensional array (representing optgroups): ```php $countries = [ 'Europe' => [ - 'CZ' => 'Czech republic', + 'CZ' => 'Czech Republic', 'SK' => 'Slovakia', 'GB' => 'United Kingdom', ], @@ -179,92 +199,167 @@ $countries = [ ]; ``` -For select boxes, the first item often has a special meaning, it serves as a call-to-action. Use the `setPrompt()` method to add such an entry. +In select boxes, the first item often has a special meaning, serving as a prompt to action. Use the `setPrompt()` method to add such an item. ```php $form->addSelect('country', 'Country:', $countries) - ->setPrompt('Pick a country'); + ->setPrompt('Choose a country'); ``` -You can use `setDisabled(['CZ', 'SK'])` to disable individual items. +Use `setDisabled(['CZ', 'SK'])` to disable individual items. -The element automatically checks that there has been no forgery and that the selected item is actually one of the offered ones and has not been disabled. The `getRawValue()` method can be used to retrieve the submitted item without this important check. +The control automatically checks that no forgery occurred and that the selected item is indeed one of the offered ones and was not disabled. The `getRawValue()` method can be used to retrieve the submitted item without this important check. -When default value is set, it also checks that it is one of the offered items, otherwise it throws an exception. This check can be turned off with `checkDefaultValue(false)`. +When setting the default selected item, it also checks that it is one of the offered ones, otherwise it throws an exception. This check can be disabled using `checkDefaultValue(false)`. -addMultiSelect(string|int $name, $label=null, array $items=null): MultiSelectBox .[method] -========================================================================================== +addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +============================================================================================================ -Adds multichoice select box (class [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Returns the array of keys of the selected items. The `getSelectedItems()` method returns values instead of keys. +Adds a select box for selecting multiple items (class [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Returns an array of the keys of the selected items. The `getSelectedItems()` method returns the values instead of keys. ```php $form->addMultiSelect('countries', 'Countries:', $countries); ``` -We pass the array of items as the third parameter, or by the `setItems()` method. The array of items can also be two-dimensional. +Pass the array of offered items as the third parameter or using the `setItems()` method. Items can also be a two-dimensional array. -You can use `setDisabled(['CZ', 'SK'])` to disable individual items. +Use `setDisabled(['CZ', 'SK'])` to disable individual items. -The element automatically checks that there has been no forgery and that the selected items are actually one of the offered ones and have not been disabled. The `getRawValue()` method can be used to retrieve submitted items without this important check. +The control automatically checks that no forgery occurred and that the selected items are indeed among the offered ones and were not disabled. The `getRawValue()` method can be used to retrieve submitted items without this important check. -When default values are set, it also checks that they are one of the offered items, otherwise it throws an exception. This check can be turned off with `checkDefaultValue(false)`. +When setting default selected items, it also checks that they are among the offered ones, otherwise it throws an exception. This check can be disabled using `checkDefaultValue(false)`. addUpload(string|int $name, $label=null): UploadControl .[method] ================================================================= -Adds file upload field (class [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returns the [FileUpload |http:request#FileUpload] object, even if the user has not uploaded a file, which can be find out by the `FileUpload::hasFile()` method. +Adds a file upload field (class [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returns a [FileUpload |http:request#FileUpload] object, even if the user did not upload any file, which can be checked using the `FileUpload::hasFile()` method. ```php $form->addUpload('avatar', 'Avatar:') - ->addRule($form::IMAGE, 'Avatar must be JPEG, PNG, GIF or WebP') - ->addRule($form::MAX_FILE_SIZE, 'Maximum size is 1 MB', 1024 * 1024); + ->addRule($form::Image, 'Avatar must be JPEG, PNG, GIF, WebP or AVIF.') + ->addRule($form::MaxFileSize, 'Maximum size is 1 MB.', 1024 * 1024); ``` -If the file did not upload correctly, the form was not submitted successfully and an error is displayed. I.e. it is not necessary to check the `FileUpload::isOk()` method. +If the file fails to upload correctly, the form is not successfully submitted, and an error is displayed. That is, upon successful submission, it is not necessary to check the `FileUpload::isOk()` method. -Do not trust the original file name returned by method `FileUpload::getName()`, a client could send a malicious filename with the intention to corrupt or hack your application. +Never trust the original file name returned by the `FileUpload::getName()` method; the client could have sent a malicious file name with the intent to damage or hack your application. -Rules `MIME_TYPE` and `IMAGE` detect required type of file or image by its signature. The integrity of the entire file is not checked. You can find out if an image is not corrupted for example by trying to [load it|http:request#toImage]. +The `MimeType` and `Image` rules detect the required type based on the file's signature and do not verify its integrity. Whether an image is corrupted can be determined, for example, by attempting to [load it |http:request#toImage]. addMultiUpload(string|int $name, $label=null): UploadControl .[method] ====================================================================== -Adds multiple file upload field (class [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returns an array of objects [FileUpload |http:request#FileUpload]. The `FileUpload::hasFile()` method will return `true` for each of them. +Adds a field for uploading multiple files at once (class [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returns an array of [FileUpload |http:request#FileUpload] objects. The `FileUpload::hasFile()` method for each of them will return `true`. ```php $form->addMultiUpload('files', 'Files:') - ->addRule($form::MAX_LENGTH, 'A maximum of %d files can be uploaded', 10); + ->addRule($form::MaxLength, 'Maximum of %d files can be uploaded.', 10); // Added period +``` + +If any file fails to upload correctly, the form is not successfully submitted, and an error is displayed. That is, upon successful submission, it is not necessary to check the `FileUpload::isOk()` method for each file. + +Never trust the original file names returned by the `FileUpload::getName()` method; the client could have sent malicious file names with the intent to damage or hack your application. + +The `MimeType` and `Image` rules detect the required type based on the file's signature and do not verify its integrity. Whether an image is corrupted can be determined, for example, by attempting to [load it |http:request#toImage]. + + +addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +====================================================================================== + +Adds a field that allows the user to easily enter a date consisting of year, month, and day (class [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). + +As a default value, it accepts objects implementing the `DateTimeInterface`, a string containing time, or a number representing a UNIX timestamp. The same applies to the arguments of the `Min`, `Max`, or `Range` rules, which define the minimum and maximum allowed dates. + +```php +$form->addDate('date', 'Date:') + ->setDefaultValue(new DateTime) + ->addRule($form::Min, 'The date must be at least one month old.', new DateTime('-1 month')); +``` + +By default, it returns a `DateTimeImmutable` object. Using the `setFormat()` method, you can specify a [text format|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] or a timestamp: + +```php +$form->addDate('date', 'Date:') + ->setFormat('Y-m-d'); +``` + + +addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== + +Adds a field that allows the user to easily enter a time consisting of hours, minutes, and optionally seconds (class [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). + +As a default value, it accepts objects implementing the `DateTimeInterface`, a string containing time, or a number representing a UNIX timestamp. Only the time information from these inputs is used; the date is ignored. The same applies to the arguments of the `Min`, `Max`, or `Range` rules, which define the minimum and maximum allowed times. If the set minimum value is higher than the maximum, a time range spanning midnight is created. + +```php +$form->addTime('time', 'Time:', withSeconds: true) + ->addRule($form::Range, 'Time must be between %s and %s.', ['12:30', '13:30']); // Use %s for time strings +``` + +By default, it returns a `DateTimeImmutable` object (with the date set to January 1, year 1). Using the `setFormat()` method, you can specify a [text format|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: + +```php +$form->addTime('time', 'Time:') + ->setFormat('H:i'); +``` + + +addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=================================================================================================================== + +Adds a field that allows the user to easily enter both date and time, consisting of year, month, day, hours, minutes, and optionally seconds (class [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). + +As a default value, it accepts objects implementing the `DateTimeInterface`, a string containing time, or a number representing a UNIX timestamp. The same applies to the arguments of the `Min`, `Max`, or `Range` rules, which define the minimum and maximum allowed date and time. + +```php +$form->addDateTime('datetime', 'Date and Time:') + ->setDefaultValue(new DateTime) + ->addRule($form::Min, 'The date must be at least one month old.', new DateTime('-1 month')); +``` + +By default, it returns a `DateTimeImmutable` object. Using the `setFormat()` method, you can specify a [text format|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] or a timestamp: + +```php +$form->addDateTime('datetime') + ->setFormat(DateTimeControl::FormatTimestamp); ``` -If one of the files fails to upload correctly, the form was not submitted successfully and an error is displayed. I.e. it is not necessary to check the `FileUpload::isOk()` method. -Do not trust the original file names returned by method `FileUpload::getName()`, a client could send a malicious filename with the intention to corrupt or hack your application. +addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=================================================================================== + +Adds a color picker field (class [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). The color is returned as a string in the format `#rrggbb`. If the user does not make a selection, it returns black `#000000`. -Rules `MIME_TYPE` and `IMAGE` detect required type of file or image by its signature. The integrity of the entire file is not checked. You can find out if an image is not corrupted for example by trying to [load it|http:request#toImage]. +```php +$form->addColor('color', 'Color:') + ->setDefaultValue('#3C8ED7'); +``` -addHidden(string|int $name, string $default=null): HiddenField .[method] -======================================================================== +addHidden(string|int $name, ?string $default=null): HiddenField .[method] +========================================================================= -Adds hidden field (class [HiddenField |api:Nette\Forms\Controls\HiddenField]). +Adds a hidden field (class [HiddenField |api:Nette\Forms\Controls\HiddenField]). ```php $form->addHidden('userid'); ``` -Use `setNullable()` to change it to return `null` instead of an empty string. The [addFilter()|validation#Modifying Input Values] allows you to change the submitted value. +Use `setNullable()` to make it return `null` instead of an empty string. The [addFilter() |validation#Modifying Input Values] method allows modifying the submitted value. + +Although the control is hidden, **it is important to realize** that its value can still be modified or spoofed by an attacker. Always thoroughly verify and validate all received values on the server side to prevent security risks associated with data manipulation. addSubmit(string|int $name, $caption=null): SubmitButton .[method] ================================================================== -Adds submit button (class [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). +Adds a submit button (class [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). ```php -$form->addSubmit('submit', 'Register'); +$form->addSubmit('submit', 'Submit'); ``` It is possible to have more than one submit button in the form: @@ -274,7 +369,7 @@ $form->addSubmit('register', 'Register'); $form->addSubmit('cancel', 'Cancel'); ``` -To find out which of them was clicked, use: +To determine which one was clicked, use: ```php if ($form['register']->isSubmittedBy()) { @@ -282,13 +377,13 @@ if ($form['register']->isSubmittedBy()) { } ``` -If you don't want to validate the form when a submit button is pressed (such as *Cancel* or *Preview* buttons), you can turn it off with [setValidationScope()|validation#Disabling Validation]. +If you do not want to validate the entire form when a button is pressed (for example, for *Cancel* or *Preview* buttons), use [setValidationScope() |validation#Disabling Validation]. addButton(string|int $name, $caption): Button .[method] ======================================================= -Adds button (class [Button |api:Nette\Forms\Controls\Button]) without submit function. It is useful for binding other functionality to id, for example a JavaScript action. +Adds a button (class [Button |api:Nette\Forms\Controls\Button]) that does not have a submit function. It can therefore be used for other functions, e.g., calling a JavaScript function on click. ```php $form->addButton('raise', 'Raise salary') @@ -296,22 +391,22 @@ $form->addButton('raise', 'Raise salary') ``` -addImageButton(string|int $name, string $src=null, string $alt=null): ImageButton .[method] -=========================================================================================== +addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +============================================================================================= -Adds submit button in form of an image (class [ImageButton |api:Nette\Forms\Controls\ImageButton]). +Adds a submit button in the form of an image (class [ImageButton |api:Nette\Forms\Controls\ImageButton]). ```php -$form->addImageButton('submit', '/path/to/image'); +$form->addImageButton('submit', '/path/to/image.png', 'Submit'); ``` -When using multiple submit buttons, you can find out which one was clicked with `$form['submit']->isSubmittedBy()`. +When using multiple submit buttons, you can determine which one was clicked using `$form['submit']->isSubmittedBy()`. addContainer(string|int $name): Container .[method] =================================================== -Adds a sub-form (class [Container|api:Nette\Forms\Container]), or a container, which can be treated the same way as a form. That means you can use methods like `setDefaults()` or `getValues()`. +Adds a sub-form (class [Container|api:Nette\Forms\Container]), or a container, into which other controls can be added in the same way as they are added to the form. Methods like `setDefaults()` or `getValues()` work as well. ```php $sub1 = $form->addContainer('first'); @@ -323,7 +418,7 @@ $sub2->addText('name', 'Your name:'); $sub2->addEmail('email', 'Email:'); ``` -The sent data is then returned as a multidimensional structure: +The submitted data is then returned as a multi-dimensional structure: ```php [ @@ -342,66 +437,66 @@ The sent data is then returned as a multidimensional structure: Overview of Settings ==================== -For all elements we can call the following methods (see [API documentation|https://api.nette.org/forms/master/Nette/Forms/Controls.html] for a complete overview): +For all controls, we can call the following methods (see the [API documentation|https://api.nette.org/forms/master/Nette/Forms/Controls.html] for a complete overview): .[table-form-methods language-php] | `setDefaultValue($value)` | sets the default value -| `getValue()` | get current value -| `setOmitted()` | [#omitted values] -| `setDisabled()` | [#disabling inputs] +| `getValue()` | get the current value +| `setOmitted()` | [#Omitted Values] +| `setDisabled()` | [#Disabling Inputs] Rendering: .[table-form-methods language-php] -| `setCaption($caption)` | change the caption of the item -| `setTranslator($translator)` | sets [translator|best-practices:translations#Form Translation] -| `setHtmlAttribute($name, $value)` | sets the [HTML attribute |rendering#HTML attributes] of the element +| `setCaption($caption)` | changes the control's label +| `setTranslator($translator)` | sets the [translator |rendering#Translating] +| `setHtmlAttribute($name, $value)` | sets an [HTML attribute |rendering#HTML Attributes] for the element | `setHtmlId($id)` | sets the HTML attribute `id` -| `setHtmlType($type)` | sets HTML attribute `type` -| `setHtmlName($name)` | sets HTML attribute `name` -| `setOption($key, $value)` | sets [rendering data|rendering#Options] +| `setHtmlType($type)` | sets the HTML attribute `type` +| `setHtmlName($name)` | sets the HTML attribute `name` +| `setOption($key, $value)` | [sets rendering options |rendering#Options] Validation: .[table-form-methods language-php] -| `setRequired()` | [mandatory field |validation] -| `addRule()` | set [validation rule |validation#Rules] -| `addCondition()`, `addConditionOn()` | set [validation condition|validation#Conditions] -| `addError($message)` | [passing error message|validation#processing-errors] +| `setRequired()` | makes the control [required |validation] +| `addRule()` | adds a [validation rule |validation#Rules] +| `addCondition()`, `addConditionOn()` | sets a [validation condition |validation#Conditions] +| `addError($message)` | [adds an error message |validation#Processing Errors] -The following methods can be called for the `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` items: +For controls `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, the following methods can be called: .[table-form-methods language-php] -| `setNullable()` | sets whether getValue() returns `null` instead of empty string -| `setEmptyValue($value)` | sets the special value which is treated as empty string -| `setMaxLength($length)` | sets the maximum number of allowed characters -| `addFilter($filter)` | [modifying Input Values |validation#Modifying Input Values] +| `setNullable()` | sets whether getValue() returns `null` instead of an empty string +| `setEmptyValue($value)` | sets a special value that is considered an empty string +| `setMaxLength($length)` | sets the maximum allowed number of characters +| `addFilter($filter)` | [modifies input |validation#Modifying Input Values] Omitted Values --------------- +============== -If you are not interested in the value entered by the user, we can use `setOmitted()` to omit it from the result provided by the `$form->getValues​()` method or passed to handlers. This is suitable for various passwords for verification, antispam fields, etc. +If we are not interested in the value filled in by the user, we can use `setOmitted()` to exclude it from the result of the `$form->getValues()` method or from the data passed to handlers. This is useful for various password confirmation fields, anti-spam controls, etc. ```php $form->addPassword('passwordVerify', 'Password again:') ->setRequired('Fill your password again to check for typo') - ->addRule($form::EQUAL, 'Password mismatch', $form['password']) + ->addRule($form::Equal, 'Passwords do not match', $form['password']) ->setOmitted(); ``` Disabling Inputs ----------------- +================ -In order to disable an input, you can call `setDisabled()`. Such a field cannot be edited by the user. +Controls can be disabled using `setDisabled()`. A disabled control cannot be edited by the user. ```php $form->addText('username', 'User name:') ->setDisabled(); ``` -Note that the browser does not send the disabled fields to the server at all, so you won't even find them in the data returned by the `$form->getValues()` function. +Disabled controls are not sent by the browser to the server at all, so you won't find them in the data returned by the `$form->getValues()` function. However, if you set `setOmitted(false)`, Nette will include their default value in this data. -If you are setting a default value for a field, you must do so only after disabling it: +When `setDisabled()` is called, the **control's value is cleared** for security reasons. If you are setting a default value, it is necessary to do so after disabling it: ```php $form->addText('username', 'User name:') @@ -409,26 +504,31 @@ $form->addText('username', 'User name:') ->setDefaultValue($userName); ``` +An alternative to disabled controls are controls with the HTML `readonly` attribute, which the browser does send to the server. Although the control is read-only, **it is important to realize** that its value can still be modified or spoofed by an attacker. + Custom Controls =============== -Besides wide range of built-in form controls you can add custom controls to the form as follows: +Besides the wide range of built-in form controls, you can add custom controls to the form in this way: ```php $form->addComponent(new DateInput('Date:'), 'date'); // alternative syntax: $form['date'] = new DateInput('Date:'); ``` -There is a way to define new form methods for adding custom elements (eg `$form->addZip()`). These are the so-called extension methods. The downside is that code hints in editors won't work for them. +.[note] +The form is a descendant of the [Container |component-model:#Container] class, and individual controls are descendants of the [Component |component-model:#Component] class. + +There is a way to define new form methods for adding custom controls (e.g., `$form->addZip()`). These are called extension methods. The disadvantage is that code completion in editors will not work for them. ```php use Nette\Forms\Container; -// adds method addZip(string $name, string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, string $label = null) { +// adds method addZip(string $name, ?string $label = null) +Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { return $form->addText($name, $label) - ->addRule($form::PATTERN, 'At least 5 numbers', '[0-9]{5}'); + ->addRule($form::Pattern, 'At least 5 numbers', '[0-9]{5}'); }); // usage @@ -439,7 +539,7 @@ $form->addZip('zip', 'ZIP code:'); Low-Level Fields ================ -To add an item to the form, you don't have to call `$form->addXyz()`. Form items can be introduced exclusively in templates instead. This is useful if you, for example, need to generate dynamic items: +It's also possible to use controls that are only written in the template and not added to the form using any of the `$form->addXyz()` methods. For example, when listing records from a database where we don't know in advance how many there will be or what their IDs will be, and we want to display a checkbox or radio button for each row, we can simply code it in the template: ```latte {foreach $items as $item} @@ -447,13 +547,13 @@ To add an item to the form, you don't have to call `$form->addXyz()`. Form items {/foreach} ``` -After submission, you can retrieve the values: +And after submission, we retrieve the value: ```php -$data = $form->getHttpData($form::DATA_TEXT, 'sel[]'); -$data = $form->getHttpData($form::DATA_TEXT | $form::DATA_KEYS, 'sel[]'); +$data = $form->getHttpData($form::DataText, 'sel[]'); +$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); ``` -In the first parameter, you specify element type (`DATA_FILE` for `type=file`, `DATA_LINE` for one-line inputs like `text`, `password` or `email` and `DATA_TEXT` for the rest). The second parameter matches HTML attribute `name`. If you need to preserve keys, you can combine the first parameter with `DATA_KEYS`. This is useful for `select`, `radioList` or `checkboxList`. +where the first parameter is the element type (`DataFile` for `type=file`, `DataLine` for single-line inputs like `text`, `password`, `email`, etc., and `DataText` for all others) and the second parameter `sel[]` corresponds to the HTML name attribute. We can combine the element type with the `DataKeys` value, which preserves the keys of the elements. This is particularly useful for `select`, `radioList`, and `checkboxList`. -The `getHttpData()` returns sanitized input. In this case, it will always be array of valid UTF-8 strings, no matter what the attacker sent by the form. It's an alternative to working with `$_POST` or `$_GET` directly if you want to receive safe data. +Crucially, `getHttpData()` returns a sanitized value. In this case, it will always be an array of valid UTF-8 strings, regardless of what an attacker might try to submit to the server. This is analogous to working directly with `$_POST` or `$_GET`, but with the significant difference that it always returns clean data, just as you are accustomed to with standard Nette form controls. diff --git a/forms/en/in-presenter.texy b/forms/en/in-presenter.texy index ea99821341..a1543bcaa1 100644 --- a/forms/en/in-presenter.texy +++ b/forms/en/in-presenter.texy @@ -2,15 +2,15 @@ Forms in Presenters ******************* .[perex] -Nette Forms make it dramatically easier to create and process web forms. In this chapter, you will learn how to use forms inside presenters. +Nette Forms significantly simplify the creation and processing of web forms. In this chapter, you will learn how to use forms inside presenters. -If you are interested in using them completely standalone without the rest of the framework, there is a guide for [standalone forms|standalone]. +If you are interested in using them completely standalone without the rest of the framework, there is a guide for [standalone usage|standalone]. First Form ========== -We will try to write a simple registration form. Its code will look like this: +Let's try writing a simple registration form. Its code will be as follows: ```php use Nette\Application\UI\Form; @@ -22,19 +22,19 @@ $form->addSubmit('send', 'Sign up'); $form->onSuccess[] = [$this, 'formSucceeded']; ``` -and in the browser the result should look like this: +and in the browser, it will be displayed like this: [* form-en.webp *] -The form in the presenter is an object of the class `Nette\Application\UI\Form`, its predecessor `Nette\Forms\Form` is intended for standalone use. We added the fields name, password and sending button to it. Finally, the line with `$form->onSuccess` says that after submission and successful validation, the `$this->formSucceeded()` method should be called. +A form in a presenter is an object of the `Nette\Application\UI\Form` class; its predecessor `Nette\Forms\Form` is intended for standalone use. We added controls named name, password, and a submit button. Finally, the line `$form->onSuccess[] = [$this, 'formSucceeded'];` states that after submission and successful validation, the method `$this->formSucceeded()` should be called. -From the presenter's point of view, the form is a common component. Therefore, it is treated as a component and incorporated into the presenter using [factory method |application:components#Factory Methods]. It will look like this: +From the presenter's perspective, the form is a regular component. Therefore, it is treated as a component and integrated into the presenter using a [factory method |application:components#Factory Methods]. It will look like this: -```php .{file:app/Presenters/HomepagePresenter.php} +```php .{file:app/Presentation/HomePresenter.php} use Nette; use Nette\Application\UI\Form; -class HomepagePresenter extends Nette\Application\UI\Presenter +class HomePresenter extends Nette\Application\UI\Presenter { protected function createComponentRegistrationForm(): Form { @@ -52,60 +52,58 @@ class HomepagePresenter extends Nette\Application\UI\Presenter // $data->name contains name // $data->password contains password $this->flashMessage('You have successfully signed up.'); - $this->redirect('Homepage:'); + $this->redirect('Home:'); } } ``` -And render in template is done using `{control}` tag: +And in the template, the form is rendered using the `{control}` tag: -```latte .{file:app/Presenters/templates/Homepage/default.latte} +```latte .{file:app/Presentation/Home/default.latte} <h1>Registration</h1> {control registrationForm} ``` -And that is all :-) We have a functional and perfectly [secured |#Vulnerability Protection] form. +And that's basically everything :-) We have a functional and perfectly [secured |#Vulnerability Protection] form. -Now you're probably thinking that it was too fast, wondering how it's possible that the method `formSucceeded()` is called and what the parameters it gets. Sure, you're right, this deserves an explanation. +Now you're probably thinking it was too fast, wondering how it's possible that the `formSucceeded()` method is called and what parameters it receives. Yes, you're right, this deserves an explanation. -Nette comes up with a cool mechanism, which we call [Hollywood style |application:components#Hollywood style]. Instead of having to constantly ask if something has happened ("was the form submitted?", "was it validly submitted?" or "was it not forged?"), you say to the framework "when the form is validly completed, call this method" and leave further work on it. If you program in JavaScript, you are familiar with this style of programming. You write functions that are called when a certain [event|nette:glossary#Events] occurs. And the language passes the appropriate arguments to them. +Nette introduces a refreshing mechanism called [Hollywood style |application:components#Hollywood Style]. Instead of you, as a developer, having to constantly ask if something happened ('was the form submitted?', 'was it submitted validly?', and 'was it not forged?'), you tell the framework 'when the form is validly filled, call this method' and leave the subsequent work to it. If you program in JavaScript, you are intimately familiar with this style of programming. You write functions that are called when a certain [event |nette:glossary#Events] occurs. And the language passes the appropriate arguments to them. -This is how the above presenter code is built. Array `$form->onSuccess` represents the list of PHP callbacks that Nette will call when the form is submitted and filled in correctly. -Within the [presenter's life cycle |application:presenters#Life Cycle of Presenter] it is a so-called signal, so they are called after the `action*` method and before the `render*` method. -And it passes to each callback the form itself in the first parameter and the sent data as object [ArrayHash |utils:arrays#ArrayHash] in the second. You can omit the first parameter if you do not need the form object. The second parameter can be even more handy, but about that [later|#Mapping to Classes]. +This is precisely how the presenter code above is constructed. The `$form->onSuccess` array represents a list of PHP callbacks that Nette calls the moment the form is submitted and correctly filled (i.e., it is valid). Within the [presenter life cycle |application:presenters#Presenter Life Cycle], this is a so-called signal, so they are called after the `action*` method and before the `render*` method. And to each callback, it passes the form itself as the first parameter and the submitted data as an [ArrayHash |utils:arrays#ArrayHash] object (or stdClass, or a custom class) as the second. You can omit the first parameter if you don't need the form object. The second parameter can be smarter, but more on that [later |#Mapping to Classes]. -The `$data` object contains the `name` and `password` properties with the data entered by the user. Usually we send the data directly for further processing, which can be, for example, insertion into the database. However, an error may occur during processing, for example, the username is already taken. In this case, we pass the error back to the form using `addError()` and let it redrawn, with an error message: +The `$data` object contains the `name` and `password` properties with the data entered by the user. Usually, we send the data directly for further processing, which might be, for example, insertion into a database. However, an error might occur during processing, for instance, the username is already taken. In such a case, we pass the error back to the form using `addError()` and let it be rendered again, along with the error message. ```php $form->addError('Sorry, username is already in use.'); ``` -In addition to `onSuccess`, there is also `onSubmit`: callbacks are always called after the form is submitted, even if it is not filled in correctly. And finally `onError`: callbacks are called only if the submission is not valid. They are even called if we invalidate the form in `onSuccess` or `onSubmit` using `addError()`. +Besides `onSuccess`, there is also `onSubmit`: callbacks are called whenever the form is submitted, even if it is not filled correctly. And also `onError`: callbacks are called only if the submission is not valid. They are even called if we invalidate the form in `onSuccess` or `onSubmit` using `addError()`. -After processing the form, we will redirect to the next page. This prevents the form from being unintentionally resubmitted by clicking the *refresh*, *back* button, or moving the browser history. +After processing the form, we redirect to another page. This prevents the unwanted resubmission of the form by using the *refresh*, *back* button, or navigating through browser history. -Try adding more [form controls|controls]. +Try adding other [form controls|controls]. Access to Controls ================== -The form is a component of the presenter, in our case named `registrationForm` (after the name of the factory method `createComponentRegistrationForm`), so anywhere in the presenter you can get to the form using: +The form is a component of the presenter, in our case named `registrationForm` (after the factory method name `createComponentRegistrationForm`), so anywhere in the presenter, you can access the form using: ```php $form = $this->getComponent('registrationForm'); // alternative syntax: $form = $this['registrationForm']; ``` -Also individual form controls are components, so you can access them in the same way: +Individual form controls are also components, so you can access them in the same way: ```php $input = $form->getComponent('name'); // or $input = $form['name']; $button = $form->getComponent('send'); // or $button = $form['send']; ``` -Controls are removed using unset: +Controls are removed using `unset`: ```php unset($form['name']); @@ -115,27 +113,26 @@ unset($form['name']); Validation Rules ================ -The word *valid* was used several times, but the form has no validation rules yet. Let's fix it. +The word *valid* was mentioned, but the form doesn't have any validation rules yet. Let's fix that. -The name will be mandatory, so we will mark it with the method `setRequired()`, whose argument is the text of the error message that will be displayed if the user does not fill it. If no argument is given, the default error message is used. +The name will be mandatory, so we mark it using the `setRequired()` method. Its argument is the text of the error message displayed if the user doesn't fill in the name. If the argument is omitted, a default error message is used. ```php $form->addText('name', 'Name:') - ->setRequired('Please fill your name.'); + ->setRequired('Please enter your name.'); ``` -Try to submit the form without the name filled in and you will see that an error message is displayed and the browser or server will reject it until you fill it. +Try submitting the form without filling in the name, and you'll see an error message displayed, and the browser or server will reject it until you fill in the field. -At the same time, you will not be able cheat the system by typing only spaces in the input, for example. No way. Nette automatically trims left and right whitespace. Try it. It's something you should always do with every single-line input, but it's often forgotten. Nette does it automatically. (You can try to fool the forms and send a multiline string as the name. Even here, Nette won't be fooled and the line breaks will change to spaces.) +At the same time, you can't cheat the system by entering only spaces in the field, for example. No way. Nette automatically trims leading and trailing whitespace. Try it out. It's something you should always do with every single-line input, but it's often forgotten. Nette does it automatically. (You can try to fool the form and send a multi-line string as the name. Even here, Nette won't be tricked, and line breaks will be converted to spaces.) -The form is always validated on the server side, but JavaScript validation is also generated, which is quick and the user knows of the error immediately, without having to send the form to the server. This is handled by the script `netteForms.js`. -Insert it into the layout template: +The form is always validated on the server side, but JavaScript validation is also generated, which runs instantly, and the user learns about the error immediately, without needing to submit the form to the server. This is handled by the `netteForms.js` script. Include it in your layout template: ```latte -<script src="https://nette.github.io/resources/js/3/netteForms.min.js"></script> +<script src="https://unpkg.com/nette-forms@3"></script> ``` -If you look in the source code of the page with form, you may notice that Nette inserts the required fields into elements with a CSS class `required`. Try adding the following style to the template, and the "Name" label will be red. Elegantly, we mark the required fields for the users: +If you look at the source code of the page with the form, you might notice that Nette wraps required controls in elements with the CSS class `required`. Try adding the following stylesheet to your template, and the 'Name' label will be red. This elegantly highlights required fields for users: ```latte <style> @@ -143,57 +140,57 @@ If you look in the source code of the page with form, you may notice that Nette </style> ``` -Additional validation rules will be added by method `addRule()`. The first parameter is rule, the second is again the text of the error message, and the optional validation rule argument can follow. What does that mean? +We add further validation rules using the `addRule()` method. The first parameter is the rule, the second is again the text of the error message, and an argument for the validation rule may follow. What does that mean? -The form will get another optional input *age* with the condition, that it has to be a number (`addInteger()`) and in certain boundaries (`$form::RANGE`). And here we will use the third argument of `addRule()`, the range itself: +Let's extend the form with a new optional field 'age', which must be an integer (`addInteger()`) and also within an allowed range (`Form::Range`). Here we will use the third parameter of the `addRule()` method to pass the required range to the validator as a pair `[min, max]`: ```php $form->addInteger('age', 'Age:') - ->addRule($form::RANGE, 'You must be older 18 years and be under 120.', [18, 120]); + ->addRule($form::Range, 'Age must be between 18 and 120.', [18, 120]); ``` .[tip] -If the user does not fill in the field, the validation rules will not be verified, because the field is optional. +If the user doesn't fill in the field, the validation rules will not be checked, as the element is optional. -Obviously room for a small refactoring is available. In the error message and in the third parameter, the numbers are listed in duplicate, which is not ideal. If we were creating a [multilingual form|best-practices:translations] and the message containing numbers would have to be translated into multiple languages, it would make it more difficult to change values. For this reason, substitute characters `%d` can be used: +This creates room for a small refactoring. In the error message and the third parameter, the numbers are duplicated, which is not ideal. If we were creating [multilingual forms |rendering#Translating] and the message containing numbers were translated into multiple languages, changing the values would become difficult. For this reason, placeholders `%d` can be used, and Nette will substitute the values: ```php - ->addRule($form::RANGE, 'You must be older %d years and be under %d.', [18, 120]); + ->addRule($form::Range, 'Age must be between %d and %d years.', [18, 120]); ``` -Let's return to the *password* field, make it *required*, and verify the minimum password length (`$form::MIN_LENGTH`), again using the substitute characters in the message: +Let's return to the `password` control, make it required as well, and also verify the minimum password length (`Form::MinLength`), again using a placeholder in the message: ```php $form->addPassword('password', 'Password:') ->setRequired('Pick a password') - ->addRule($form::MIN_LENGTH, 'Your password has to be at least %d long', 8); + ->addRule($form::MinLength, 'Your password must be at least %d characters long.', 8); ``` -We will add a field `passwordVerify` to the form, where the user enters the password again, for checking. Using validation rules, we check whether both passwords are the same (`$form::EQUAL`). And as an argument we give a reference to the first password using [square brackets |#Access to Controls]: +Let's add another field `passwordVerify` to the form, where the user enters the password again for confirmation. Using validation rules, we check if both passwords are the same (`Form::Equal`). As the argument, we provide a reference to the first password using [square brackets |#Access to Controls]: ```php $form->addPassword('passwordVerify', 'Password again:') ->setRequired('Fill your password again to check for typo') - ->addRule($form::EQUAL, 'Password mismatch', $form['password']) + ->addRule($form::Equal, 'Passwords do not match.', $form['password']) ->setOmitted(); ``` -Using `setOmitted()`, we marked an element whose value we don't really care about and which exists only for validation. Its value is not passed to `$data`. +Using `setOmitted()`, we marked a control whose value we don't actually care about and which exists only for validation purposes. Its value is not passed to `$data`. -We have a fully functional form with validation in PHP and JavaScript. Nette's validation capabilities are much broader, you can create conditions, display and hide parts of a page according to them, etc. You can find out everything in the chapter on [form validation|validation]. +With this, we have a fully functional form with validation in both PHP and JavaScript. Nette's validation capabilities are much broader; conditions can be created, parts of the page can be shown or hidden based on them, etc. You will learn everything in the chapter on [form validation|validation]. Default Values ============== -We often set default values for form controls: +We commonly set default values for form controls: ```php $form->addEmail('email', 'Email') ->setDefaultValue($lastUsedEmail); ``` -It is often useful to set default values for all controls at once. For example, when the form is used to edit records. We read the record from the database and set it as default values: +It's often useful to set default values for all controls simultaneously. For example, when the form is used for editing records. We read the record from the database and set the default values: ```php //$row = ['name' => 'John', 'age' => '33', /* ... */]; @@ -206,50 +203,49 @@ Call `setDefaults()` after defining the controls. Rendering the Form ================== -By default, the form is rendered as a table. The individual controls follows basic web accessibility guidelines. All labels are generated as `<label>` elements and are associated with their inputs, clicking on the label moves the cursor on the input. +By default, the form is rendered as a table. Individual controls adhere to basic web accessibility rules - all labels are written as `<label>` elements and associated with their respective form controls. Clicking on the label automatically focuses the cursor in the form field. -We can set any HTML attributes for each element. For example, add a placeholder: +We can set arbitrary HTML attributes for each control. For example, add a placeholder: ```php $form->addInteger('age', 'Age:') ->setHtmlAttribute('placeholder', 'Please fill in the age'); ``` -There are really a lot of ways to render a form, so it's dedicated [chapter about rendering|rendering]. +There are truly many ways to render a form, so a [separate chapter on rendering|rendering] is dedicated to it. Mapping to Classes ================== -Let's go back to the `formSucceeded()` method, which in the second parameter `$data` gets the sent data as an `ArrayHash` object. Because this is a generic class, something like `stdClass`, we will lack some convenience when working with it, such as code completition for properties in editors or static code analysis. This could be solved by having a specific class for each form, whose properties represent the individual controls. E.g.: +Let's return to the `formSucceeded()` method, which receives the submitted data in the second parameter `$data` as an `ArrayHash` object (or `stdClass`). Because it's a generic class, similar to `stdClass`, we lack certain conveniences when working with it, such as property autocompletion in editors or static code analysis. This could be solved by having a specific class for each form, whose properties represent the individual controls. E.g.: ```php class RegistrationFormData { - /** @var string */ - public $name; - /** @var int */ - public $age; - /** @var string */ - public $password; + public string $name; + public ?int $age; + public string $password; } ``` -As of PHP 8.0, you can use this elegant notation that uses a constructor: +Alternatively, you can use a constructor: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` -How to tell Nette to return data as objects of this class? Easier than you think. All you have to do is specify the class as the type of the `$data` parameter in the handler: +Properties of the data class can also be enums, and they will be automatically mapped. .{data-version:3.2.4} + +How do we tell Nette to return data as objects of this class? Easier than you might think. Simply specify the class as the type of the `$data` parameter in the handler method: ```php public function formSucceeded(Form $form, RegistrationFormData $data): void @@ -260,16 +256,16 @@ public function formSucceeded(Form $form, RegistrationFormData $data): void } ``` -You can also specify `array` as the type and then it will pass the data as an array. +You can also specify `array` as the type, and then the data will be passed as an array. -In a similar way, you can use the `getValues()` method, which we pass as a class name or object to hydrate as a parameter: +Similarly, you can use the `getValues()` method, passing the class name or an object to hydrate as a parameter: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -If the forms consist of a multi-level structure composed of containers, create a separate class for each one: +If the forms have a multi-level structure composed of containers, create a separate class for each: ```php $form = new Form; @@ -286,22 +282,24 @@ class PersonFormData class RegistrationFormData { public PersonFormData $person; - public int $age; + public ?int $age; public string $password; } ``` -The mapping then knows from the `$person` property type that it should map the container to the `PersonFormData` class. If the property would contain an array of containers, provide the `array` type and pass the class to be mapped directly to the container: +The mapping then infers from the type of the `$person` property that it should map the container to the `PersonFormData` class. If the property were to contain an array of containers, specify the type `array` and pass the class to be mapped directly to the container: ```php $person->setMappedType(PersonFormData::class); ``` +You can generate a proposal for the form's data class using the `Nette\Forms\Blueprint::dataClass($form)` method, which prints it to the browser page. Then, simply click to select and copy the code into your project. .{data-version:3.1.15} + Multiple Submit Buttons ======================= -If the form has more than one button, we usually need to distinguish which one was pressed. We can create own function for each button. Set it as a handler for the `onClick` [event|nette:glossary#Events]: +If the form has more than one button, we usually need to distinguish which one was pressed. We can create a separate handler function for each button. Set it as a handler for the `onClick` [event |nette:glossary#Events]: ```php $form->addSubmit('save', 'Save') @@ -311,7 +309,7 @@ $form->addSubmit('delete', 'Delete') ->onClick[] = [$this, 'deleteButtonPressed']; ``` -These handlers are also called only in the case form is valid, as in the case of the `onSuccess` event. The difference is that the first parameter can be the submit button object instead of the form, depending on the type you specify: +These handlers are called only if the form is validly filled (unless validation is disabled for the button), just like the `onSuccess` event. The difference is that the first parameter passed can be the submit button object instead of the form, depending on the type hint you specify: ```php public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) @@ -321,15 +319,15 @@ public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) } ``` -When a form is submitted with the <kbd>Enter</kbd> key, it is treated as if it had been submitted with the first button. +When the form is submitted by pressing the <kbd>Enter</kbd> key, it is treated as if it were submitted by the first submit button. Event onAnchor ============== -When you build a form in a factory method (such as `createComponentRegistrationForm`), it doesn't yet know if it has been submitted or the data it was submitted with. But there are cases where we need to know the submitted values, perhaps it's depend on them what the form will look like, or they are used for dependent selectboxes, etc. +When you build a form in a factory method (like `createComponentRegistrationForm`), it doesn't yet know if it has been submitted or with what data. However, there are cases where we need to know the submitted values, perhaps the form's appearance depends on them, or they are needed for dependent select boxes, etc. -Therefore, you can have the code that builds the form called when it is anchored, i.e. it is already linked to the presenter and knows its submitted data. We will put such code into the `$onAnchor` array: +Therefore, you can have the code that builds the form called only when it is 'anchored,' meaning it is already connected to the presenter and knows its submitted data. Place such code in the `$onAnchor` array: ```php $country = $form->addSelect('country', 'Country:', $this->model->getCountries()); @@ -347,34 +345,33 @@ $form->onAnchor[] = function () use ($country, $city) { Vulnerability Protection ======================== -Nette Framework puts a great effort to be safe and since forms are the most common user input, Nette forms are as good as impenetrable. All is maintained dynamically and transparently, nothing has to be set manually. +Nette Framework places great emphasis on security and therefore meticulously ensures the security of forms. It does this completely transparently and requires no manual setup. -In addition to protecting the forms against attacks targeted at well-known vulnerabilities such as [Cross-Site Scripting (XSS) |nette:glossary#cross-site-scripting-xss] and [Cross-Site Request Forgery (CSRF)|nette:glossary#cross-site-request-forgery-csrf], it does a lot of small security tasks that you no longer have to think about. +Besides protecting forms against attacks like [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] and [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], it performs many small security measures that you no longer need to think about. -For example, it filters out all control characters from the inputs and checks the validity of the UTF-8 encoding, so that the data from the form will always be clean. For select boxes and radio lists, it verifies that the selected items were actually from the offered ones and there was no forgery. We've already mentioned that for single-line text input, it removes end-of-line characters that an attacker could send there. For multiline inputs, it normalizes the end-of-line characters. And so on. +For example, it filters all control characters from inputs and checks the validity of UTF-8 encoding, ensuring the data from the form is always clean. For select boxes and radio lists, it verifies that the selected items were actually among the offered ones and that no forgery occurred. We've already mentioned that for single-line text inputs, it removes end-of-line characters that an attacker might send. For multi-line inputs, it normalizes end-of-line characters. And so on. -Nette fixes security vulnerabilities for you that most programmers have no idea exist. +Nette handles security risks for you that many programmers aren't even aware exist. -The mentioned CSRF attack is that an attacker lures the victim to visit a page that silently executes a request in the victim's browser to the server where the victim is currently logged in, and the server believes that the request was made by the victim at will. Therefore, Nette prevents the form from being submitted via POST from another domain. If for some reason you want to turn off protection and allow the form to be submitted from another domain, use: +The mentioned CSRF attack involves an attacker luring a victim to a page that silently executes a request in the victim's browser to the server where the victim is logged in. The server then believes the request was made by the victim willingly. Therefore, Nette prevents POST forms from being submitted from a different domain. If, for some reason, you want to disable this protection and allow form submission from another domain, use: ```php -$form->allowCrossOrigin(); // ATTENTION! Turns off protection! +$form->allowCrossOrigin(); // WARNING! Disables protection! ``` -This protection uses a SameSite cookie named `_nss`. SameSite cookie protection may not be 100% reliable, so it's a good idea to turn on token protection: +This protection uses a SameSite cookie named `_nss`. SameSite cookie protection might not be 100% reliable, so it's advisable to also enable token protection: ```php $form->addProtection(); ``` -It's strongly recommended to apply this protection to the forms in an administrative part of your application which changes sensitive data. The framework protects against a CSRF attack by generating and validating authentication token that is stored in a session (the argument is the error message shown if the token has expired). That's why it is necessary to have an session started before displaying the form. In the administration part of the website, the session is usually already started, due to the user's login. -Otherwise, start the session with the method `Nette\Http\Session::start()`. +It is strongly recommended to apply this protection to forms in the administrative parts of your website that modify sensitive data. The framework defends against CSRF attacks by generating and verifying an authorization token stored in the session. Therefore, it is necessary to have a session started before displaying the form. In the administrative part of a website, the session is usually already started due to user login. Otherwise, start the session using `Nette\Http\Session::start()`. Using One Form in Multiple Presenters ===================================== -If you need to use one form in more than one presenter, we recommend that you create a factory for it, which you then pass on to the presenter. A suitable location for such a class is, for example, the directory `app/Forms`. +If you need to use the same form in multiple presenters, we recommend creating a factory for it, which you then inject into the presenters. A suitable location for such a class is, for example, the `app/Forms` directory. The factory class might look like this: @@ -393,12 +390,12 @@ class SignInFormFactory } ``` -We ask the class to produce the form in the factory method for components in the presenter: +We request the class to produce the form in the component factory method within the presenter: ```php -public function __construct(SignInFormFactory $formFactory) -{ - $this->formFactory = $formFactory; +public function __construct( + private SignInFormFactory $formFactory, +) { } protected function createComponentSignInForm(): Form @@ -411,7 +408,7 @@ protected function createComponentSignInForm(): Form } ``` -The form processing handler can also be delivered from the factory: +The form processing handler can also be provided by the factory itself: ```php use Nette\Application\UI\Form; @@ -431,4 +428,4 @@ class SignInFormFactory } ``` -So, we have a quick introduction to forms in Nette. Try looking in the [examples |https://github.com/nette/forms/tree/master/examples] directory in the distribution for more inspiration. +So, we've covered a quick introduction to forms in Nette. Try looking in the [examples |https://github.com/nette/forms/tree/master/examples] directory in the distribution for more inspiration. diff --git a/forms/en/rendering.texy b/forms/en/rendering.texy index 4359bb5c18..ccb19528ba 100644 --- a/forms/en/rendering.texy +++ b/forms/en/rendering.texy @@ -1,33 +1,35 @@ Forms Rendering *************** -Forms' appearances can differ greatly. In fact, there are two extremes. One side is the need to render a set of very similar forms all over again, with little to none effort. Usually administrations and back-ends. +The appearance of forms can be very diverse. In practice, we may encounter two extremes. On one hand, there is the need to render numerous forms in an application that are visually identical, and we appreciate the easy rendering without a template using `$form->render()`. This is typically the case with administrative interfaces. -The other side is tiny sweet forms, each one being a piece of art. Their layout can best be written in HTML. Of course, besides those extremes, there are many forms just in between. +On the other hand, there are diverse forms where each one is unique. Their appearance is best described using HTML in the form template. And of course, besides these two extremes, we encounter many forms that fall somewhere in between. -Latte -===== +Rendering With Latte +==================== -The [Latte templating system|latte:] fundamentally facilitates the rendering of forms and their elements. First, we'll show how to render forms manually, element by element, to gain full control over the code. Later we will show how to [automate |#Automatic rendering] such rendering. +The [Latte templating system |latte:] significantly simplifies the rendering of forms and their elements. First, we'll demonstrate how to render forms manually, element by element, to gain full control over the code. Later, we will show how such rendering can be [automated |#Automatic Rendering]. + +You can generate the Latte template for the form using the `Nette\Forms\Blueprint::latte($form)` method, which outputs it to the browser page. Then, simply click to select the code and copy it into your project. .{data-version:3.1.15} `{control}` ----------- -The easiest way to render a form is to write in a template: +The simplest way to render a form is to write in the template: ```latte {control signInForm} ``` -The look of the rendered form can be changed by configuring [#Renderer] and [individual controls|#HTML Attributes]. +The appearance of the rendered form can be influenced by configuring the [#Renderer] and [individual controls |#HTML Attributes]. `n:name` -------- -It is extremely easy to link the form definition in PHP code with HTML code. Just add the `n:name` attributes. That's how easy it is! +Linking the form definition in PHP code with HTML code is extremely easy. Just add the `n:name` attributes. It's that simple! ```php protected function createComponentSignInForm(): Form @@ -54,10 +56,9 @@ protected function createComponentSignInForm(): Form </form> ``` -The look of the resulting HTML code is entirely in your hands. If you use the `n:name` attribute with `<select>`, `<button>` or `<textarea>` elements, their internal content is automatically filled in. -In addition, the `<form n:name>` tag creates a local variable `$form` with the drawn form object and the closing `</form>` draws all undrawn hidden elements (the same applies to `{form} ... {/form}`). +You have full control over the appearance of the resulting HTML code. If you use the `n:name` attribute with `<select>`, `<button>`, or `<textarea>` elements, their inner content is automatically populated. Additionally, the `<form n:name>` tag creates a local variable `$form` containing the rendered form object, and the closing `</form>` tag renders any unrendered hidden controls (the same applies to `{form} ... {/form}`). -However, we must not forget to render possible error messages. Both those that were added to individual elements by the `addError()` method (using `{inputError}`) and those added directly to the form (returned by `$form->getOwnErrors()`): +However, we must not forget to render potential error messages. This includes errors added to individual controls using the `addError()` method (rendered via `{inputError}`) and errors added directly to the form (returned by `$form->getOwnErrors()`): ```latte <form n:name=signInForm class=form> @@ -79,7 +80,7 @@ However, we must not forget to render possible error messages. Both those that w </form> ``` -More complex form elements, such as RadioList or CheckboxList, can be rendered item by item: +More complex form controls, like RadioList or CheckboxList, can be rendered item by item like this: ```latte {foreach $form[gender]->getItems() as $key => $label} @@ -88,16 +89,10 @@ More complex form elements, such as RadioList or CheckboxList, can be rendered i ``` -Code Proposal `{formPrint}` .[#toc-formprint] ---------------------------------------------- - -You can generate a similar Latte code for a form using the `{formPrint}` tag. If you place it in a template, you will see the draft code instead of the normal rendering. Then just select it and copy it into your project. - - `{label}` `{input}` ------------------- -Don't you want to think for each element what HTML element to use for it in the template, whether `<input>`, `<textarea>` etc.? The solution is the universal `{input}` tag: +Prefer not to think about which HTML element to use for each control in the template, whether it's `<input>`, `<textarea>`, etc.? The solution is the universal `{input}` tag: ```latte <form n:name=signInForm class=form> @@ -121,7 +116,7 @@ Don't you want to think for each element what HTML element to use for it in the If the form uses a translator, the text inside the `{label}` tags will be translated. -Again, more complex form elements, such as RadioList or CheckboxList, can be rendered item by item: +Again, more complex form controls, such as RadioList or CheckboxList, can be rendered item by item: ```latte {foreach $form[gender]->items as $key => $label} @@ -129,20 +124,19 @@ Again, more complex form elements, such as RadioList or CheckboxList, can be ren {/foreach} ``` -To render the `<input>` itself in the Checkbox item, use `{input myCheckbox:}`. HTML attributes must be separated by a comma `{input myCheckbox:, class: required}`. +To render just the `<input>` for a Checkbox control, use `{input myCheckbox:}`. In this case, always separate HTML attributes with a comma: `{input myCheckbox:, class: required}`. `{inputError}` -------------- -Prints an error message for the form element, if it has one. The message is usually wrapped in an HTML element for styling. -Avoiding rendering an empty element if there is no message can be elegantly done with `n:ifcontent`: +Displays the error message for a form control, if one exists. The message is usually wrapped in an HTML element for styling. Preventing the rendering of an empty element when there is no message can be elegantly achieved using `n:ifcontent`: ```latte <span class=error n:ifcontent>{inputError $input}</span> ``` -We can detect the presence of an error using the `hasErrors()` method and set the class of the parent element accordingly: +We can check for the presence of an error using the `hasErrors()` method and set the class of the parent element accordingly: ```latte <div n:class="$form[username]->hasErrors() ? 'error'"> @@ -152,11 +146,16 @@ We can detect the presence of an error using the `hasErrors()` method and set th ``` +`{form}` +-------- + +The tags `{form signInForm}...{/form}` are an alternative to `<form n:name="signInForm">...</form>`. + + Automatic Rendering ------------------- -With the `{input}` and `{label}` tags, we can easily create a generic template for any form. It will iterate and render all of its elements sequentially, except for hidden elements, which are rendered automatically when the form is terminated with the `</form>` tag. -It will expect the name of the rendered form in the `$form` variable. +Thanks to the `{input}` and `{label}` tags, we can easily create a generic template for any form. It will iterate through and render all its controls, except for hidden controls, which are rendered automatically when the form is closed with the `</form>` tag. It expects the name of the form to be rendered in the `$form` variable. ```latte <form n:name=$form class=form> @@ -173,16 +172,15 @@ It will expect the name of the rendered form in the `$form` variable. </form> ``` -The used self-closing pair tags `{label .../}` show the labels coming from the form definition in the PHP code. +The self-closing pair tags `{label .../}` used here display the labels originating from the form definition in the PHP code. -You can save this generic template in the `basic-form.latte` file and to render the form, just include it and pass the form name (or instance) to the `$form` parameter: +Save this generic template, for example, in the file `basic-form.latte`. To render the form, simply include it and pass the form name (or instance) to the `$form` parameter: ```latte {include basic-form.latte, form: signInForm} ``` -If you would like to influence the appearance of one particular form and draw one element differently, then the easiest way is to prepare blocks in the template that can be overwritten later. -Blocks can also have [dynamic names |latte:template-inheritance#dynamic-block-names], so you can insert the name of the element to be drawn into them. For example: +If you want to modify the appearance of a specific form during rendering, perhaps rendering one control differently, the easiest way is to prepare blocks in the template that can be subsequently overridden. Blocks can also have [dynamic names |latte:template-inheritance#Dynamic Block Names], allowing you to insert the name of the rendered control. For example: ```latte ... @@ -191,7 +189,7 @@ Blocks can also have [dynamic names |latte:template-inheritance#dynamic-block-na ... ``` -For the element e.g. `username` this creates the block `input-username`, which can be easily overridden by using the tag [{embed} |latte:template-inheritance#unit-inheritance]: +For a control named, e.g., `username`, this creates the block `input-username`, which can be easily overridden using the [{embed} |latte:template-inheritance#Unit Inheritance] tag: ```latte {embed basic-form.latte, form: signInForm} @@ -203,7 +201,7 @@ For the element e.g. `username` this creates the block `input-username`, which c {/embed} ``` -Alternatively, the entire contents of the `basic-form.latte` template can be [defined |latte:template-inheritance#definitions] as a block, including the `$form` parameter: +Alternatively, the entire content of the `basic-form.latte` template can be [defined |latte:template-inheritance#Definitions] as a block, including the `$form` parameter: ```latte {define basic-form, $form} @@ -213,7 +211,7 @@ Alternatively, the entire contents of the `basic-form.latte` template can be [de {/define} ``` -This will make it slightly easier to use: +This makes calling it slightly simpler: ```latte {embed basic-form, signInForm} @@ -221,7 +219,7 @@ This will make it slightly easier to use: {/embed} ``` -You only need to import the block in one place, at the beginning of the layout template: +The block only needs to be imported in one place, at the beginning of the layout template: ```latte {import basic-form.latte} @@ -231,18 +229,18 @@ You only need to import the block in one place, at the beginning of the layout t Special Cases ------------- -If you only need to render the inner content of a form without `<form>` & `</form>` HTML tags, for example, in an AJAX request, you can open and close the form with `{formContext} … {/formContext}`. It works similarly to `{form}` in a logical sense, here it allows you to use other tags to draw form elements, but at the same time it doesn't draw anything. +If you need to render only the inner part of the form without the `<form>` HTML tags, for example, when sending snippets, hide them using the `n:tag-if` attribute: ```latte -{formContext signForm} +<form n:name=signInForm n:tag-if=false> <div> <label n:name=username>Username: <input n:name=username></label> {inputError username} </div> -{/formContext} +</form> ``` -Tag `formContainer` helps with rendering of inputs inside a form container. +The `{formContainer}` tag helps with rendering controls inside a form container. ```latte <p>Which news you wish to receive:</p> @@ -256,8 +254,8 @@ Tag `formContainer` helps with rendering of inputs inside a form container. ``` -Without Latte -============= +Rendering Without Latte +======================= The easiest way to render a form is to call: @@ -265,16 +263,16 @@ The easiest way to render a form is to call: $form->render(); ``` -The look of the rendered form can be changed by configuring [#Renderer] and [individual controls|#HTML Attributes]. +The appearance of the rendered form can be influenced by configuring the [#Renderer] and [individual controls |#HTML Attributes]. Manual Rendering ---------------- -Each form element has methods that generate the HTML code for the form field and label. They can return it as either a string or a [Nette\Utils\Html|utils:html-elements] object: +Each form control has methods that generate the HTML code for the form field and its label. They can return it either as a string or a [Nette\Utils\Html |utils:html-elements] object: -- `getControl(): Html|string` returns the HTML code of the element -- `getLabel($caption = null): Html|string|null` returns the HTML code of the label, if any +- `getControl(): Html|string` returns the HTML code of the control +- `getLabel($caption = null): Html|string|null` returns the HTML code of the label, if it exists This allows the form to be rendered element by element: @@ -299,35 +297,34 @@ This allows the form to be rendered element by element: <?php $form->render('end') ?> ``` -While for some elements `getControl()` returns a single HTML element (e.g. `<input>`, `<select>` etc.), for others it returns a whole piece of HTML code (CheckboxList, RadioList). -In this case, you can use methods that generate individual inputs and labels, for each item separately: +While for some controls `getControl()` returns a single HTML element (e.g., `<input>`, `<select>`, etc.), for others it returns a complete piece of HTML code (CheckboxList, RadioList). In such cases, you can use methods that generate individual inputs and labels for each item separately: - `getControlPart($key = null): ?Html` returns the HTML code of a single item - `getLabelPart($key = null): ?Html` returns the HTML code for the label of a single item .[note] -These methods are prefixed with `get` for historical reasons, but `generate` would be better, as it creates and returns a new `Html` element on each call. +These methods have the prefix `get` for historical reasons, but `generate` would be more appropriate, as they create and return a new `Html` element upon each call. Renderer ======== -It is an object that provides rendering of the form. It can be set by the `$form->setRenderer` method. It is passed control when the `$form->render()` method is called. +This is an object responsible for rendering the form. It can be set using the `$form->setRenderer()` method. Control is passed to it when the `$form->render()` method is called. -If we don't set a custom renderer, the default renderer [api:Nette\Forms\Rendering\DefaultFormRenderer] will be used. This will render the form elements as an HTML table. The output looks like this: +If we do not set a custom renderer, the default renderer [api:Nette\Forms\Rendering\DefaultFormRenderer] will be used. This renders the form controls into an HTML table. The output looks like this: ```latte <table> <tr class="required"> <th><label class="required" for="frm-name">Name:</label></th> - <td><input type="text" class="text" name="name" id="frm-name" value=""></td> + <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> </tr> <tr class="required"> <th><label class="required" for="frm-age">Age:</label></th> - <td><input type="text" class="text" name="age" id="frm-age" value=""></td> + <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> </tr> <tr> @@ -335,11 +332,11 @@ If we don't set a custom renderer, the default renderer [api:Nette\Forms\Renderi ... ``` -It's up to you, whether to use a table or not, and many web designers prefer different markups, for example a list. We may configure `DefaultFormRenderer` so it would not render into a table at all. We just have to set proper [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. The first index always represents an area and the second one it's element. All respective areas are shown in the picture: +Whether to use a table for the form structure is debatable, and many web designers prefer different markup, such as a definition list. Therefore, we will reconfigure `DefaultFormRenderer` to render the form as a list. Configuration is done by editing the [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers] array. The first index always represents an area, and the second its attribute. The individual areas are shown in the picture: [* form-areas-en.webp *] -By default a group of `controls` is wrapped in `<table>`, and every `pair` is a table row `<tr>` containing a pair of `label` and `control` (cells `<th>` and `<td>`). Let's change all those wrapper elements. We will wrap `controls` into `<dl>`, leave `pair` by itself, put `label` into `<dt>` and wrap `control` into `<dd>`: +By default, the `controls` group is wrapped in `<table>`, each `pair` represents a table row `<tr>`, and the `label` and `control` pair are cells `<th>` and `<td>`. Now we will change the wrapping elements. We will place the `controls` area into a `<dl>` container, leave the `pair` area without a container, put the `label` into `<dt>`, and finally wrap the `control` with `<dd>` tags: ```php $renderer = $form->getRenderer(); @@ -351,18 +348,18 @@ $renderer->wrappers['control']['container'] = 'dd'; $form->render(); ``` -Results into the following snippet: +This results in the following HTML code: ```latte <dl> <dt><label class="required" for="frm-name">Name:</label></dt> - <dd><input type="text" class="text" name="name" id="frm-name" value=""></dd> + <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> <dt><label class="required" for="frm-age">Age:</label></dt> - <dd><input type="text" class="text" name="age" id="frm-age" value=""></dd> + <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> <dt><label>Gender:</label></dt> @@ -370,25 +367,25 @@ Results into the following snippet: </dl> ``` -Wrappers can affect many attributes. For example: +The wrappers array allows influencing many other attributes: -- add special CSS classes to each form input -- distinguish between odd and even lines -- make required and optional draw differently -- set, whether error messages are shown above the form or close to each element +- adding CSS classes to individual types of form controls +- distinguishing odd and even rows with CSS classes +- visually distinguishing required and optional items +- determining whether error messages are displayed directly next to controls or above the form Options ------- -The behavior of Renderer can also be controlled by setting *options* on individual form elements. This way you can set the tooltip that is displayed next to the input field: +The behavior of the Renderer can also be controlled by setting *options* on individual form controls. This way, you can set a description that appears next to the input field: ```php $form->addText('phone', 'Number:') ->setOption('description', 'This number will remain hidden'); ``` -If we want to place HTML content into it, we use [Html |utils:html-elements] class. +If we want to place HTML content within it, we use the [Html |utils:html-elements] class: ```php use Nette\Utils\Html; @@ -400,19 +397,19 @@ $form->addText('phone', 'Phone:') ``` .[tip] -Html element can be also used instead of label: `$form->addCheckbox('conditions', $label)`. +An Html element can also be used instead of a label: `$form->addCheckbox('conditions', $label)`. Grouping Inputs --------------- -Renderer allows to group elements into visual groups (fieldsets): +The Renderer allows grouping controls into visual groups (fieldsets): ```php $form->addGroup('Personal data'); ``` -Creating new group activates it - all elements added further are added to this group. You may build a form like this: +After creating a new group, it becomes active, and every newly added control is also added to it. Thus, the form can be built this way: ```php $form = new Form; @@ -428,31 +425,33 @@ $form->addText('city', 'City:'); $form->addSelect('country', 'Country:', $countries); ``` +The renderer draws groups first, followed by the controls that do not belong to any group. + Bootstrap Support ----------------- -You can find [examples |https://github.com/nette/forms/tree/master/examples] of configuration of Renderer for [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] and [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] +You can find examples in the [examples directory |https://github.com/nette/forms/tree/master/examples] showing how to configure the Renderer for [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58], and [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php]. HTML Attributes =============== -You can set any HTML attributes to form controls using `setHtmlAttribute(string $name, $value = true)`: +To set any HTML attributes for form controls, use the `setHtmlAttribute(string $name, $value = true)` method: ```php $form->addInteger('number', 'Number:') ->setHtmlAttribute('class', 'big-number'); $form->addSelect('rank', 'Order by:', ['price', 'name']) - ->setHtmlAttribute('onchange', 'submit()'); // calls JS function submit() on change + ->setHtmlAttribute('onchange', 'submit()'); // submit the form on change -// applying on <form> +// To set attributes of the <form> element itself $form->setHtmlAttribute('id', 'myForm'); ``` -Setting input type: +Specifying the type of control: ```php $form->addText('tel', 'Your telephone:') @@ -460,8 +459,10 @@ $form->addText('tel', 'Your telephone:') ->setHtmlAttribute('placeholder', 'Please, fill in your telephone'); ``` -We can set HTML attribute to individual items in radio or checkbox lists with different values for each of them. -Note the colon after `style:` to ensure that the value is selected by key: +.[warning] +Setting the type and other attributes is only for visual purposes. Verification of input correctness must occur on the server side, which you ensure by choosing an appropriate [form control |controls] and specifying [validation rules |validation]. + +For individual items in radio or checkbox lists, we can set an HTML attribute with different values for each. Notice the colon after `style:`, which ensures the value is selected based on the key: ```php $colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue']; @@ -478,12 +479,11 @@ Renders: <label><input type="checkbox" name="colors[]" value="b">blue</label> ``` -For a logical HTML attribute (which has no value, such as `readonly`), you can use a question mark: +For setting boolean attributes, such as `readonly`, we can use the notation with a question mark: ```php -$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue']; $form->addCheckboxList('colors', 'Colors:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // use array for multiple keys, e.g. ['r', 'g'] + ->setHtmlAttribute('readonly?', 'r'); // use an array for multiple keys, e.g., ['r', 'g'] ``` Renders: @@ -494,8 +494,7 @@ Renders: <label><input type="checkbox" name="colors[]" value="b">blue</label> ``` -For selectboxes, the `setHtmlAttribute()` method sets the attributes of the `<select>` element. If we want to set the attributes for each -`<option>`, we will use the method `setOptionAttribute()`. Also, the colon and question mark used above work: +For select boxes, the `setHtmlAttribute()` method sets the attributes of the `<select>` element. If we want to set attributes for individual `<option>` elements, we use the `setOptionAttribute()` method. The colon and question mark notations mentioned above also work: ```php $form->addSelect('colors', 'Colors:', $colors) @@ -519,7 +518,7 @@ Prototypes An alternative way to set HTML attributes is to modify the template from which the HTML element is generated. The template is an `Html` object and is returned by the `getControlPrototype()` method: ```php -$input = $form->addInteger('number'); +$input = $form->addInteger('number', 'Number:'); $html = $input->getControlPrototype(); // <input> $html->class('big-number'); // <input class="big-number"> ``` @@ -531,13 +530,10 @@ $html = $input->getLabelPrototype(); // <label> $html->class('distinctive'); // <label class="distinctive"> ``` -For Checkbox, CheckboxList and RadioList items you can influence the element template that wraps the item. It is returned by `getContainerPrototype()`. By default it is an "empty" element, so nothing is rendered, but by giving it a name it will be rendered: +For Checkbox, CheckboxList, and RadioList controls, you can influence the template of the element that wraps the entire control. It is returned by `getContainerPrototype()`. By default, it is an "empty" element, so nothing is rendered, but by giving it a name, it will be rendered: ```php $input = $form->addCheckbox('send'); -echo $input->getControl(); -// <label><input type="checkbox" name="send"></label> - $html = $input->getContainerPrototype(); $html->setName('div'); // <div> $html->class('check'); // <div class="check"> @@ -545,14 +541,49 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -In the case of CheckboxList and RadioList it is also possible to influence the item separator pattern returned by the method `getSeparatorPrototype()`. By default, it is an element `<br>`. If you change it to a pair element, it will wrap the individual items instead of separating them. -It is also possible to influence the HTML element template of the item labels, which returns `getItemLabelPrototype()`. +In the case of CheckboxList and RadioList, you can also influence the template of the separator for individual items, returned by the `getSeparatorPrototype()` method. By default, it is the `<br>` element. If you change it to a pair element, it will wrap the individual items instead of separating them. Furthermore, you can influence the template of the HTML element for the labels of individual items, returned by `getItemLabelPrototype()`. + + +Translating +=========== + +If you are developing a multilingual application, you will likely need to render the form in different language versions. The Nette Framework defines a translation interface for this purpose: [api:Nette\Localization\Translator]. Nette does not have a default implementation; you can choose from several ready-made solutions found on [Componette |https://componette.org/search/localization] according to your needs. Their documentation explains how to configure the translator. + +Forms support outputting texts via the translator. We pass it using the `setTranslator()` method: + +```php +$form->setTranslator($translator); +``` + +From this point on, not only all labels but also all error messages or items in select boxes will be translated into the target language. + +It is possible to set a different translator for individual form controls or disable translation completely by setting the value to `null`: + +```php +$form->addSelect('carModel', 'Model:', $cars) + ->setTranslator(null); +``` + +For [validation rules |validation], specific parameters are also passed to the translator. For example, for the rule: + +```php +$form->addPassword('password', 'Password:') + ->addRule($form::MinLength, 'Password must be at least %d characters long', 8); +``` + +the translator is called with these parameters: + +```php +$translator->translate('Password must be at least %d characters long', 8); +``` + +and thus can choose the correct plural form for the word `characters` based on the count. Event onRender ============== -Just before the form is rendered, we can have our code invoked. This can, for example, add HTML classes to the form elements for proper display. We add the code to the `onRender` array: +Just before the form is rendered, we can have our code invoked. This code can, for example, add HTML classes to the form controls for correct display. We add the code to the `onRender` array: ```php $form->onRender[] = function ($form) { diff --git a/forms/en/standalone.texy b/forms/en/standalone.texy index 279456e506..0260668a0e 100644 --- a/forms/en/standalone.texy +++ b/forms/en/standalone.texy @@ -2,15 +2,15 @@ Forms Used Standalone ********************* .[perex] -Nette Forms make it dramatically easier to create and process web forms. You can use them in your applications completely on their own without the rest of the framework, which we'll demonstrate in this chapter. +Nette Forms dramatically simplify the creation and processing of web forms. You can use them in your applications completely standalone, without the rest of the framework, as demonstrated in this chapter. -However, if you use Nette Application and presenters, there is a guide for you: [forms in presenters|in-presenter]. +However, if you use Nette Application and presenters, there is a dedicated guide for you: [forms in presenters |in-presenter]. First Form ========== -We will try to write a simple registration form. Its code will look like this ("full code":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851): +Let's try writing a simple registration form. Its code will be as follows ("full code":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851): ```php use Nette\Forms\Form; @@ -21,39 +21,39 @@ $form->addPassword('password', 'Password:'); $form->addSubmit('send', 'Sign up'); ``` -And let's render it: +And let's render it very easily: ```php $form->render(); ``` -and the result should look like this: +The result in the browser should look like this: [* form-en.webp *] -The form is an object of the `Nette\Forms\Form` class (the `Nette\Application\UI\Form` class is used in presenters). We added the controls name, password and sending button to it. +The form is an object of the `Nette\Forms\Form` class (the `Nette\Application\UI\Form` class is used in presenters). We added controls named 'name', 'password', and a submit button to it. -Now we will revive the form. By asking `$form->isSuccess()`, we will find out whether the form was submitted and whether it was filled in validly. If so, we will dump the data. After the definition of the form we will add: +Now let's bring the form to life. By querying `$form->isSuccess()`, we find out if the form was submitted and if it was filled in validly. If so, we will output the data. After the form definition, add: ```php if ($form->isSuccess()) { - echo 'The form has been filled in and submitted correctly'; + echo 'Form was filled and submitted successfully'; $data = $form->getValues(); - // $data->name contains name - // $data->password contains password + // $data->name contains the name + // $data->password contains the password var_dump($data); } ``` -Method `getValues()` returns the sent data in the form of an object [ArrayHash |utils:arrays#ArrayHash]. We will show how to change this [later |#Mapping to Classes]. The variable `$data` contains keys `name` and `password` with data entered by the user. +The `getValues()` method returns the submitted data as an [ArrayHash |utils:arrays#ArrayHash] object. We will show how to change this [later |#Mapping to Classes]. The `$data` object contains the keys `name` and `password` with the data entered by the user. -Usually we send the data directly for further processing, which can be, for example, insertion into the database. However, an error may occur during processing, for example, the username is already taken. In this case, we pass the error back to the form using `addError()` and let it redrawn, with an error message: +Usually, we send the data directly for further processing, such as inserting it into a database. However, an error might occur during processing, for example, if the username is already taken. In this case, we pass the error back to the form using `addError()` and let it be rendered again, along with the error message. ```php -$form->addError('Sorry, username is already in use.'); +$form->addError('Sorry, this username is already taken.'); ``` -After processing the form, we will redirect to the next page. This prevents the form from being unintentionally resubmitted by clicking the *refresh*, *back* button, or moving the browser history. +After processing the form, we redirect to the next page. This prevents the form from being unintentionally resubmitted by clicking the *refresh* or *back* buttons, or by navigating through browser history. By default, the form is sent using the POST method to the same page. Both can be changed: @@ -62,15 +62,15 @@ $form->setAction('/submit.php'); $form->setMethod('GET'); ``` -And that is all :-) We have a functional and perfectly [secured |#Vulnerability Protection] form. +And that's basically it :-) We have a functional and perfectly [secured |#Vulnerability Protection] form. -Try adding more [form controls|controls]. +Try adding other [form controls |controls] as well. Access to Controls ================== -The form and its individual controls are called components. They create a component tree, where the root is the form. You can access the individual controls as follows: +The form and its individual controls are called components. They form a component tree, where the form is the root. You can access individual form controls like this: ```php $input = $form->getComponent('name'); @@ -80,7 +80,7 @@ $button = $form->getComponent('send'); // alternative syntax: $button = $form['send']; ``` -Controls are removed using unset: +Controls are removed using `unset`: ```php unset($form['name']); @@ -90,27 +90,26 @@ unset($form['name']); Validation Rules ================ -The word *valid* was used here, but the form has no validation rules yet. Let's fix it. +The word *valid* was mentioned, but the form doesn't have any validation rules yet. Let's fix that. -The name will be mandatory, so we will mark it with the method `setRequired()`, whose argument is the text of the error message that will be displayed if the user does not fill it. If no argument is given, the default error message is used. +The name will be required, so we mark it with the `setRequired()` method. Its argument is the text of the error message displayed if the user doesn't fill in the name. If no argument is provided, the default error message is used. ```php $form->addText('name', 'Name:') ->setRequired('Please enter a name.'); ``` -Try to submit the form without the name filled in and you will see that an error message is displayed and the browser or server will reject it until you fill it. +Try submitting the form without filling in the name, and you'll see an error message appear. The browser or server will reject it until you fill in the field. -At the same time, you will not be able cheat the system by typing only spaces in the input, for example. No way. Nette automatically trims left and right whitespace. Try it. It's something you should always do with every single-line input, but it's often forgotten. Nette does it automatically. (You can try to fool the forms and send a multiline string as the name. Even here, Nette won't be fooled and the line breaks will change to spaces.) +At the same time, you can't cheat the system by typing only spaces into the input. No way. Nette automatically trims leading and trailing whitespace. Try it. It's something you should always do with every single-line input, but it's often forgotten. Nette does it automatically. (You can try to trick the form by sending a multi-line string as the name. Even here, Nette won't be fooled, and line breaks will be converted to spaces.) -The form is always validated on the server side, but JavaScript validation is also generated, which is quick and the user knows of the error immediately, without having to send the form to the server. This is handled by the script `netteForms.js`. -Add it to the page: +The form is always validated on the server side, but JavaScript validation is also generated. This runs instantly, and the user learns about errors immediately, without needing to submit the form to the server. This is handled by the `netteForms.js` script. Insert it into the page: ```latte -<script src="https://nette.github.io/resources/js/3/netteForms.min.js"></script> +<script src="https://unpkg.com/nette-forms@3"></script> ``` -If you look in the source code of the page with form, you may notice that Nette inserts the required fields into elements with a CSS class `required`. Try adding the following style to the template, and the "Name" label will be red. Elegantly, we mark the required fields for the users: +If you look at the source code of the page with the form, you might notice that Nette inserts required controls into elements with the CSS class `required`. Try adding the following stylesheet to the template, and the "Name" label will turn red. This elegantly highlights required controls for users: ```latte <style> @@ -118,44 +117,44 @@ If you look in the source code of the page with form, you may notice that Nette </style> ``` -Additional validation rules will be added by method `addRule()`. The first parameter is rule, the second is again the text of the error message, and the optional validation rule argument can follow. What does that mean? +We add other validation rules using the `addRule()` method. The first parameter is the rule, the second is again the text of the error message, and an optional validation rule argument can follow. What does that mean? -The form will get another optional input *age* with the condition, that it has to be a number (`addInteger()`) and in certain boundaries (`$form::RANGE`). And here we will use the third argument of `addRule()`, the range itself: +Let's extend the form with a new optional field "age", which must be an integer (`addInteger()`) and within an allowed range (`$form::Range`). Here we will use the third parameter of the `addRule()` method to pass the required range to the validator as a pair `[min, max]`: ```php $form->addInteger('age', 'Age:') - ->addRule($form::RANGE, 'You must be older 18 years and be under 120.', [18, 120]); + ->addRule($form::Range, 'Age must be between 18 and 120.', [18, 120]); ``` .[tip] -If the user does not fill in the field, the validation rules will not be verified, because the field is optional. +If the user does not fill in the field, the validation rules will not be checked, as the control is optional. -Obviously room for a small refactoring is available. In the error message and in the third parameter, the numbers are listed in duplicate, which is not ideal. If we were creating a [multilingual form|best-practices:translations] and the message containing numbers would have to be translated into multiple languages, it would make it more difficult to change values. For this reason, substitute characters `%d` can be used: +This creates room for a small refactoring. The numbers are duplicated in the error message and the third parameter, which isn't ideal. If we were creating [multilingual forms |rendering#Translating] and the message containing numbers were translated into multiple languages, changing the values would become difficult. For this reason, placeholders `%d` can be used, and Nette will fill in the values: ```php - ->addRule($form::RANGE, 'You must be older %d years and be under %d.', [18, 120]); + ->addRule($form::Range, 'Age must be between %d and %d years.', [18, 120]); ``` -Let's return to the *password* field, make it *required*, and verify the minimum password length (`$form::MIN_LENGTH`), again using the substitute characters in the message: +Let's return to the `password` control, make it required as well, and also verify the minimum password length (`$form::MinLength`), again using a placeholder in the message: ```php $form->addPassword('password', 'Password:') - ->setRequired('Pick a password') - ->addRule($form::MIN_LENGTH, 'Your password has to be at least %d long', 8); + ->setRequired('Choose a password') + ->addRule($form::MinLength, 'Password must be at least %d characters long', 8); ``` -We will add a field `passwordVerify` to the form, where the user enters the password again, for checking. Using validation rules, we check whether both passwords are the same (`$form::EQUAL`). And as an argument we give a reference to the first password using [square brackets |#Access to Controls]: +Let's add another field `passwordVerify` to the form, where the user enters the password again for verification. Using validation rules, we check if both passwords are the same (`$form::Equal`). As the parameter, we provide a reference to the first password using [square brackets |#Access to Controls]: ```php $form->addPassword('passwordVerify', 'Password again:') - ->setRequired('Fill your password again to check for typo') - ->addRule($form::EQUAL, 'Password mismatch', $form['password']) + ->setRequired('Please enter the password again for verification') + ->addRule($form::Equal, 'Passwords do not match', $form['password']) ->setOmitted(); ``` -Using `setOmitted()`, we marked an element whose value we don't really care about and which exists only for validation. Its value is not passed to `$data`. +Using `setOmitted()`, we marked a control whose value we don't really care about and which exists only for validation purposes. Its value is not passed to `$data`. -We have a fully functional form with validation in PHP and JavaScript. Nette's validation capabilities are much broader, you can create conditions, display and hide parts of a page according to them, etc. You can find out everything in the chapter on [form validation|validation]. +With this, we have a fully functional form with validation in both PHP and JavaScript. Nette's validation capabilities are much broader; you can create conditions, show and hide parts of the page based on them, etc. You will learn everything in the chapter on [form validation |validation]. Default Values @@ -168,7 +167,7 @@ $form->addEmail('email', 'Email') ->setDefaultValue($lastUsedEmail); ``` -It is often useful to set default values for all controls at once. For example, when the form is used to edit records. We read the record from the database and set it as default values: +It's often useful to set default values for all controls simultaneously, for example, when the form is used for editing records. We read the record from the database and set its values as defaults: ```php //$row = ['name' => 'John', 'age' => '33', /* ... */]; @@ -181,36 +180,33 @@ Call `setDefaults()` after defining the controls. Rendering the Form ================== -By default, the form is rendered as a table. The individual controls follows basic web accessibility guidelines. All labels are generated as `<label>` elements and are associated with their inputs, clicking on the label moves the cursor on the input. +By default, the form is rendered as a table. Individual controls adhere to basic accessibility guidelines - all labels are generated as `<label>` elements and associated with their respective form controls. Clicking on a label automatically places the cursor in the form field. -We can set any HTML attributes for each element. For example, add a placeholder: +We can set arbitrary HTML attributes for each control. For example, add a placeholder: ```php $form->addInteger('age', 'Age:') ->setHtmlAttribute('placeholder', 'Please fill in the age'); ``` -There are really a lot of ways to render a form, so it's dedicated [chapter about rendering|rendering]. +There are many ways to render a form, so a [separate chapter is dedicated to rendering |rendering]. Mapping to Classes ================== -Let's go back to form data processing. Method `getValues()` returned the submitted data as an `ArrayHash` object. Because this is a generic class, something like `stdClass`, we will lack some convenience when working with it, such as code completition for properties in editors or static code analysis. This could be solved by having a specific class for each form, whose properties represent the individual controls. E.g.: +Let's return to processing form data. The `getValues()` method returned the submitted data as an `ArrayHash` object. Since this is a generic class, like `stdClass`, we lack certain conveniences when working with it, such as property autocompletion in editors or static code analysis. This could be solved by having a specific class for each form, whose properties represent the individual controls. E.g.: ```php class RegistrationFormData { - /** @var string */ - public $name; - /** @var int */ - public $age; - /** @var string */ - public $password; + public string $name; + public ?int $age; + public string $password; } ``` -As of PHP 8.0, you can use this elegant notation that uses a constructor: +Alternatively, you can use the constructor: ```php class RegistrationFormData @@ -224,14 +220,16 @@ class RegistrationFormData } ``` -How to tell Nette to return data to us as objects of this class? Easier than you think. All you have to do is specify the class name or object to hydrate as a parameter: +Properties of the data class can also be enums, and they will be automatically mapped. .{data-version:3.2.4} + +How do we tell Nette to return data as objects of this class? Easier than you might think. All you need to do is specify the class name or the object to hydrate as a parameter: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -An `'array'` can also be specified as a parameter, and then the data returns as an array. +You can also specify `'array'` as the parameter, and the data will be returned as an array. If the forms consist of a multi-level structure composed of containers, create a separate class for each one: @@ -250,22 +248,24 @@ class PersonFormData class RegistrationFormData { public PersonFormData $person; - public int $age; + public ?int $age; public string $password; } ``` -The mapping then knows from the `$person` property type that it should map the container to the `PersonFormData` class. If the property would contain an array of containers, provide the `array` type and pass the class to be mapped directly to the container: +The mapping then knows from the `$person` property type that it should map the container to the `PersonFormData` class. If the property were to contain an array of containers, specify the `array` type and pass the class to be mapped directly to the container: ```php $person->setMappedType(PersonFormData::class); ``` +You can have a proposal for the form's data class generated using the `Nette\Forms\Blueprint::dataClass($form)` method, which will print it to the browser page. You can then simply click to select and copy the code into your project. .{data-version:3.1.15} + Multiple Submit Buttons ======================= -If the form has more than one button, we usually need to distinguish which one was pressed. The method `isSubmittedBy()` of the button returns this information to us: +If the form has more than one button, we usually need to distinguish which one was pressed. The button's `isSubmittedBy()` method returns this information: ```php $form->addSubmit('save', 'Save'); @@ -282,37 +282,36 @@ if ($form->isSuccess()) { } ``` -Do not omit the `$form->isSuccess()` to verify the validity of the data. +Do not omit the `$form->isSuccess()` check; it verifies the validity of the data. -When a form is submitted with the <kbd>Enter</kbd> key, it is treated as if it had been submitted with the first button. +When a form is submitted by pressing the <kbd>Enter</kbd> key, it is treated as if it were submitted by the first button. Vulnerability Protection ======================== -Nette Framework puts a great effort to be safe and since forms are the most common user input, Nette forms are as good as impenetrable. +Nette Framework places a strong emphasis on security and therefore meticulously ensures the proper security of forms. -In addition to protecting the forms against attack well-known vulnerabilities such as [Cross-Site Scripting (XSS) |nette:glossary#cross-site-scripting-xss] and [Cross-Site Request Forgery (CSRF)|nette:glossary#cross-site-request-forgery-csrf] it does a lot of small security tasks that you no longer have to think about. +In addition to protecting forms against well-known vulnerabilities like [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] and [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], it performs many small security measures that you no longer need to think about. -For example, it filters out all control characters from the inputs and checks the validity of the UTF-8 encoding, so that the data from the form will always be clean. For select boxes and radio lists, it verifies that the selected items were actually from the offered ones and there was no forgery. We've already mentioned that for single-line text input, it removes end-of-line characters that an attacker could send there. For multiline inputs, it normalizes the end-of-line characters. And so on. +For example, it filters all control characters from inputs and checks the validity of UTF-8 encoding, ensuring that the data from the form will always be clean. For select boxes and radio lists, it verifies that the selected items were actually among the offered options and that no forgery occurred. We've already mentioned that for single-line text inputs, it removes end-of-line characters that an attacker might send. For multi-line inputs, it normalizes end-of-line characters. And so on. -Nette fixes security vulnerabilities for you that most programmers have no idea exist. +Nette handles security risks for you that many programmers are unaware even exist. -The mentioned CSRF attack is that an attacker lures the victim to visit a page that silently executes a request in the victim's browser to the server where the victim is currently logged in, and the server believes that the request was made by the victim at will. Therefore, Nette prevents the form from being submitted via POST from another domain. If for some reason you want to turn off protection and allow the form to be submitted from another domain, use: +The mentioned CSRF attack involves an attacker luring a victim to a page that silently executes a request in the victim's browser to the server where the victim is currently logged in. The server believes that the request was made by the victim voluntarily. Therefore, Nette prevents the form from being submitted via POST from a different domain. If, for some reason, you want to disable protection and allow the form to be submitted from another domain, use: ```php -$form->allowCrossOrigin(); // ATTENTION! Turns off protection! +$form->allowCrossOrigin(); // WARNING! Disables protection! ``` -This protection uses a SameSite cookie named `_nss`. Therefore, create a form before flushing the first output so that the cookie can be sent. +This protection uses a SameSite cookie named `_nss`. Therefore, create the form object before sending the first output so that the cookie can be sent. -SameSite cookie protection may not be 100% reliable, so it's a good idea to turn on token protection: +SameSite cookie protection may not be 100% reliable, so it's advisable to also enable token protection: ```php $form->addProtection(); ``` -It's strongly recommended to apply this protection to the forms in an administrative part of your application which changes sensitive data. The framework protects against a CSRF attack by generating and validating authentication token that is stored in a session (the argument is the error message shown if the token has expired). That's why it is necessary to have an session started before displaying the form. In the administration part of the website, the session is usually already started, due to the user's login. -Otherwise, start the session with the method `Nette\Http\Session::start()`. +We strongly recommend applying this protection to forms in the administrative part of your application that modify sensitive data. The framework defends against CSRF attacks by generating and validating an authorization token stored in the session. Therefore, it is necessary to have a session started before displaying the form. In the administrative part of the website, the session is usually already started due to user login. Otherwise, start the session using the `Nette\Http\Session::start()` method. -So, we have a quick introduction to forms in Nette. Try looking in the [examples |https://github.com/nette/forms/tree/master/examples] directory in the distribution for more inspiration. +So, we've had a quick introduction to forms in Nette. Try looking in the [examples |https://github.com/nette/forms/tree/master/examples] directory in the distribution for more inspiration. diff --git a/forms/en/validation.texy b/forms/en/validation.texy index 4ad6564041..0c32b3bb85 100644 --- a/forms/en/validation.texy +++ b/forms/en/validation.texy @@ -5,133 +5,144 @@ Forms Validation Required Controls ================= -Controls are marked as required with the method `setRequired()`, whose argument is the text of the [error message|#Error Messages] that will be displayed if the user does not fill it. If no argument is given, the default error message is used. +Controls are marked as required using the `setRequired()` method. Its argument is the text of the [error message |#Error Messages] that will be displayed if the user does not fill in the control. If no argument is provided, the default error message is used. ```php $form->addText('name', 'Name:') - ->setRequired('Please fill your name.'); + ->setRequired('Please fill in your name.'); ``` Rules ===== -We add validation rules to controls with the `addRule()` method. The first parameter is the rule, the second is the [error message|#Error Messages], and the third is the validation rule argument. +We add validation rules to controls using the `addRule()` method. The first parameter is the rule, the second is the [error message |#Error Messages], and the third is the validation rule argument. ```php $form->addPassword('password', 'Password:') - ->addRule($form::MIN_LENGTH, 'Password must be at least %d characters', 8); + ->addRule($form::MinLength, 'Password must be at least %d characters long', 8); ``` -Nette comes with a number of built-in rules whose names are constants of the `Nette\Forms\Form` class: +**Validation rules are checked only if the user has filled in the control.** -We can use the following rules for all controls: +Nette comes with several predefined rules whose names are constants of the `Nette\Forms\Form` class. We can apply these rules to all controls: -| constant | description | arguments +| constant | description | argument type |------- -| `REQUIRED` | alias of `setRequired()` | - -| `FILLED` | alias of `setRequired()` | - -| `BLANK` | must not be filled | - -| `EQUAL` | value is equal to parameter | `mixed` -| `NOT_EQUAL` | value is not be equal to parameter | `mixed` -| `IS_IN` | value is equal to some element in the array | `array` -| `IS_NOT_IN` | value does not equal any element in the array | `array` -| `VALID` | input passes validation (for [#conditions]) | - - -For controls `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` the following rules can also be used: - -| `MIN_LENGTH` | minimal string length | `int` -| `MAX_LENGTH` | maximal string length | `int` -| `LENGTH` | length in range or exact length | pair `[int, int]` or `int` -| `EMAIL` | valid email address | - -| `URL` | valid URL | - -| `PATTERN` | matches regular pattern | `string` -| `PATTERN_ICASE` | like `PATTERN`, but case-insensitive | `string` -| `INTEGER` | integer | - -| `NUMERIC` | alias of `INTEGER` | - -| `FLOAT` | integer or floating point number | - -| `MIN` | minimum of the integer value | `int\|float` -| `MAX` | maximum of the integer value | `int\|float` -| `RANGE` | value in the range | pair `[int\|float, int\|float]` - -The `INTEGER`, `NUMERIC` a `FLOAT` rules automatically convert the value to integer (or float respectively). Furthermore, the `URL` rule also accepts an address without a schema (eg `nette.org`) and completes the schema (`https://nette.org`). -The expressions in `PATTERN` and `PATTERN_ICASE` must be valid for the whole value, ie as if it were wrapped in the characters `^` and `$`. - -For controls `addUpload()`, `addMultiUpload()` the following rules can also be used: - -| `MAX_FILE_SIZE` | maximal file size | `int` -| `MIME_TYPE` | MIME type, accepts wildcards (`'video/*'`) | `string\|string[]` -| `IMAGE` | uploaded file is JPEG, PNG, GIF, WebP | - -| `PATTERN` | file name matches regular expression | `string` -| `PATTERN_ICASE` | like `PATTERN`, but case-insensitive | `string` - -The `MIME_TYPE` and `IMAGE` require PHP extension `fileinfo`. Whether a file or image is of the required type is detected by its signature. The integrity of the entire file is not checked. You can find out if an image is not corrupted for example by trying to [load it|http:request#toImage]. - -For controls `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` the following rules can also be used to limit the number of selected items, respectively uploaded files: - -| `MIN_LENGTH` | minimal count | `int` -| `MAX_LENGTH` | maximal count | `int` -| `LENGTH` | count in range or exact count | pair `[int, int]` or `int` +| `Required` | required control, alias for `setRequired()` | - +| `Filled` | required control, alias for `setRequired()` | - +| `Blank` | control must not be filled | - +| `Equal` | value must be equal to the parameter | `mixed` +| `NotEqual` | value must not be equal to the parameter | `mixed` +| `IsIn` | value must be one of the items in the array | `array` +| `IsNotIn` | value must not be any of the items in the array | `array` +| `Valid` | is the control filled correctly? (for [#Conditions]) | - + + +Text inputs +----------- + +For controls `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()`, some of the following rules can also be applied: + +| `MinLength` | minimum text length | `int` +| `MaxLength` | maximum text length | `int` +| `Length` | length in range or exact length | pair `[int, int]` or `int` +| `Email` | valid email address | - +| `URL` | absolute URL | - +| `Pattern` | matches regular expression | `string` +| `PatternInsensitive` | like `Pattern`, but case-insensitive | `string` +| `Integer` | integer value | - +| `Numeric` | alias for `Integer` | - +| `Float` | number | - +| `Min` | minimum value of a numeric control | `int\|float` +| `Max` | maximum value of a numeric control | `int\|float` +| `Range` | value in range | pair `[int\|float, int\|float]` + +The validation rules `Integer`, `Numeric`, and `Float` automatically convert the value to an integer or float, respectively. Furthermore, the `URL` rule also accepts an address without a scheme (e.g., `nette.org`) and completes the scheme (`https://nette.org`). The expression in `Pattern` and `PatternInsensitive` must be valid for the entire value, i.e., as if it were wrapped in `^` and `$` characters. + + +Number of Items +--------------- + +For controls `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()`, you can also use the following rules to limit the number of selected items or uploaded files: + +| `MinLength` | minimum count | `int` +| `MaxLength` | maximum count | `int` +| `Length` | count in range or exact count | pair `[int, int]` or `int` + + +File Upload +----------- + +For controls `addUpload()`, `addMultiUpload()`, the following rules can also be used: + +| `MaxFileSize` | maximum file size in bytes | `int` +| `MimeType` | MIME type, wildcards allowed (`'video/*'`) | `string\|string[]` +| `Image` | JPEG, PNG, GIF, WebP, AVIF image | - +| `Pattern` | file name matches regular expression | `string` +| `PatternInsensitive` | like `Pattern`, but case-insensitive | `string` + +`MimeType` and `Image` require the PHP extension `fileinfo`. Whether a file or image is of the required type is detected based on its signature, and **the integrity of the entire file is not checked.** You can determine if an image is corrupted, for example, by trying to [load it |http:request#toImage]. Error Messages --------------- +============== -All predefined rules except `PATTERN` and `PATTERN_ICASE` have a default error message, so they it be omitted. However, by passing and formulating all customized messages, you will make the form more user-friendly. +All predefined rules except `Pattern` and `PatternInsensitive` have a default error message, so they can be omitted. However, by providing and formulating all custom messages tailored to your needs, you will make the form more user-friendly. -You can change the default messages in [forms:configuration], by modifying the texts in the `Nette\Forms\Validator::$messages` array or by using [translator|best-practices:translations#Form translation]. +You can change the default messages in the [configuration |forms:configuration], by modifying the texts in the `Nette\Forms\Validator::$messages` array, or by using a [translator |rendering#Translating]. -The following wildcards can be used in the text of error messages: +The following placeholder strings can be used in the text of error messages: -| `%d` | gradually replaces the rules after the arguments -| `%n$d` | replaces with the nth rule argument -| `%label` | replaces with field label (without colon) -| `%name` | replaces with field name (eg `name`) -| `%value` | replaces with value entered by the user +| `%d` | replaced sequentially by rule arguments +| `%n$d` | replaced by the n-th rule argument +| `%label` | replaced by the control label (without the colon) +| `%name` | replaced by the control name (e.g., `name`) +| `%value` | replaced by the value entered by the user ```php $form->addText('name', 'Name:') ->setRequired('Please fill in %label'); $form->addInteger('id', 'ID:') - ->addRule($form::RANGE, 'at least %d and no more than %d', [5, 10]); + ->addRule($form::Range, 'at least %d and at most %d', [5, 10]); $form->addInteger('id', 'ID:') - ->addRule($form::RANGE, 'no more than %2$d and at least %1$d', [5, 10]); + ->addRule($form::Range, 'at most %2$d and at least %1$d', [5, 10]); ``` Conditions ========== -Besides validation rules, conditions can be set. They are set much like rules, yet we use `addRule()` instead of `addCondition()` and of course, we leave it without an error message (the condition just asks): +In addition to rules, conditions can also be added. They are written similarly to rules, but instead of `addRule()`, we use the `addCondition()` method, and naturally, we don't provide an error message (the condition only asks): ```php $form->addPassword('password', 'Password:') - // if password is not longer than 8 characters ... - ->addCondition($form::MAX_LENGTH, 8) - // ... then it must contain a number - ->addRule($form::PATTERN, 'Must contain number', '.*[0-9].*'); + // if the password length is not greater than 8 + ->addCondition($form::MaxLength, 8) + // then it must contain a digit + ->addRule($form::Pattern, 'Must contain a digit', '.*[0-9].*'); ``` -Condition can be linked to a different element than the current one using `addConditionOn()`. The first parameter is a reference to the field. In the following case, the email will only be required if the checkbox is checked (ie. its value is `true`): +The condition can be linked to a control other than the current one using `addConditionOn()`. The first parameter is a reference to the control. In this example, the email will be required only if the checkbox is checked (i.e., its value is true): ```php -$form->addCheckbox('newsletters', 'send me newsletters'); +$form->addCheckbox('newsletters', 'Send me newsletters'); $form->addEmail('email', 'Email:') - // if checkbox is checked ... - ->addConditionOn($form['newsletters'], $form::EQUAL, true) - // ... require email - ->setRequired('Fill your email address'); + // if the checkbox is checked + ->addConditionOn($form['newsletters'], $form::Equal, true) + // then require the email + ->setRequired('Enter your email address'); ``` -Conditions can be grouped into complex structures with `elseCondition()` and `endCondition()` methods. +Conditions can be formed into complex structures using `elseCondition()` and `endCondition()`: ```php $form->addText(/* ... */) ->addCondition(/* ... */) // if the first condition is met - ->addConditionOn(/* ... */) // and the second condition on another element too + ->addConditionOn(/* ... */) // and the second condition on another control is also met ->addRule(/* ... */) // require this rule ->elseCondition() // if the second condition is not met ->addRule(/* ... */) // require these rules @@ -140,29 +151,29 @@ $form->addText(/* ... */) ->addRule(/* ... */); ``` -In Nette, it is very easy to react to the fulfillment or not of a condition on the JavaScript side using the `toggle()` method, see [#Dynamic JavaScript]. +In Nette, it is very easy to react to the fulfillment or non-fulfillment of a condition on the JavaScript side using the `toggle()` method, see [#Dynamic JavaScript]. -References Between Controls -=========================== +Reference to Another Control +============================ -The rule or condition argument can be a reference to another element. For example, you can dynamically validate that the `text` has as many characters as the value of the `length` field is: +You can also pass another form control as an argument to a rule or condition. The rule will then use the value entered later by the user in the browser. This can be used, for example, to dynamically validate that the `password` control contains the same string as the `password_confirm` control: ```php -$form->addInteger('length'); -$form->addText('text') - ->addRule($form::LENGTH, null, $form['length']); +$form->addPassword('password', 'Password'); +$form->addPassword('password_confirm', 'Confirm Password') + ->addRule($form::Equal, 'The passwords do not match', $form['password']); ``` Custom Rules and Conditions =========================== -Sometimes we get into a situation where the built-in validation rules in Nette are not enough and we need to validate the data from the user in our own way. In Nette this is very easy! +Sometimes we encounter situations where the built-in validation rules in Nette are insufficient, and we need to validate user data in our own way. In Nette, this is very simple! -You can pass any callback as the first parameter to the `addRule()` or `addCondition()` methods. The callback accepts the element itself as the first parameter and returns a boolean value indicating whether the validation was successful. When adding a rule using `addRule()`, additional arguments can be passed, and these are then passed as the second parameter. +You can pass any callback as the first parameter to the `addRule()` or `addCondition()` methods. The callback accepts the control itself as the first parameter and returns a boolean value indicating whether the validation was successful. When adding a rule using `addRule()`, additional arguments can be provided, which are then passed as the second parameter. -The custom set of validators can thus be created as a class with static methods: +A custom set of validators can thus be created as a class with static methods: ```php class MyValidators @@ -175,23 +186,23 @@ class MyValidators public static function validateEmailDomain(BaseControl $input, $domain) { - // additional validators + // other validators } } ``` -The usage is then very simple: +Usage is then very straightforward: ```php $form->addInteger('num') ->addRule( [MyValidators::class, 'validateDivisibility'], 'The value must be a multiple of %d', - 8 + 8, ); ``` -Custom validation rules can also be added to JavaScript. The only requirement is that the rule must be a static method. Its name for the JavaScript validator is created by concatenating the class name without backslashes `\`, the underscore `_`, and the method name. For example, write `App\MyValidators::validateDivisibility` as `AppMyValidators_validateDivisibility` and add it to the `Nette.validators` object: +Custom validation rules can also be added to JavaScript. The condition is that the rule must be a static method. Its name for the JavaScript validator is formed by concatenating the class name without backslashes `\`, an underscore `_`, and the method name. For example, `App\MyValidators::validateDivisibility` is written as `AppMyValidators_validateDivisibility` and added to the `Nette.validators` object: ```js Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { @@ -203,9 +214,9 @@ Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => Event onValidate ================ -After the form is submitted, validation is performed by checking the individual rules added by `addRule()` and then calling [event|nette:glossary#Events] `onValidate`. Its handler can be used for additional validation, typically to verify the correct combination of values in multiple form elements. +After the form is submitted, validation is performed, checking the individual rules added using `addRule()`, and subsequently, the `onValidate` [event |nette:glossary#Events] is triggered. Its handler can be used for additional validation, typically verifying the correct combination of values in multiple form controls. -If an error is detected, it is passed to the form using the `addError()` method. This can be called either on a specific element or directly on the form. +If an error is detected, it is passed to the form using the `addError()` method. This can be called either on a specific control or directly on the form. ```php protected function createComponentSignInForm(): Form @@ -228,82 +239,81 @@ public function validateSignInForm(Form $form, \stdClass $data): void Processing Errors ================= -In many cases, we discover an error when we are processing a valid form, e.g. when we write a new entry to the database and encounter a duplicate key. In this case, we pass the error back to the form using the `addError()` method. This can be called either on a specific item or directly on the form: +In many cases, we only discover an error when processing a valid form, for example, when writing a new entry to the database and encountering a duplicate key. In such a case, we again pass the error back to the form using the `addError()` method. This can be called either on a specific control or directly on the form: ```php try { $data = $form->getValues(); $this->user->login($data->username, $data->password); - $this->redirect('Homepage:'); + $this->redirect('Home:'); } catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::INVALID_CREDENTIAL) { + if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { $form->addError('Invalid password.'); } } ``` -If possible, we recommend adding the error directly to the form element, as it will appear next to it when using the default renderer. +If possible, we recommend adding the error directly to the form control, as it will be displayed next to it when using the default renderer. ```php $form['date']->addError('Sorry, this date is already taken.'); ``` -You can call `addError()` repeatedly to pass multiple error messages to a form or element. You get them with `getErrors()`. +You can call `addError()` repeatedly to pass multiple error messages to a form or control. You can retrieve them using `getErrors()`. -Note that `$form->getErrors()` returns a summary of all error messages, even those passed directly to individual elements, not just directly to the form. Error messages passed only to the form are retrieved via `$form->getOwnErrors()`. +Note that `$form->getErrors()` returns a summary of all error messages, including those passed directly to individual controls, not just directly to the form. Error messages passed only to the form can be retrieved via `$form->getOwnErrors()`. Modifying Input Values ====================== -Using the `addFilter()` method, we can modify the value entered by the user. In this example, we will tolerate and remove spaces in the zip code: +Using the `addFilter()` method, we can modify the value entered by the user. In this example, we will tolerate and remove spaces in the postal code: ```php -$form->addText('zip', 'Postcode:') +$form->addText('zip', 'Postal Code:') ->addFilter(function ($value) { - return str_replace(' ', '', $value); // remove spaces from the postcode + return str_replace(' ', '', $value); // remove spaces from the postal code }) - ->addRule($form::PATTERN, 'The postal code is not five digits', '\d{5}'); + ->addRule($form::Pattern, 'Postal code is not five digits', '\d{5}'); ``` -The filter is included between the validation rules and conditions and therefore depends on the order of the methods, ie the filter and the rule are called in the same order as is the order of the `addFilter()` and `addRule()` methods. +The filter is integrated among validation rules and conditions, and thus the order of methods matters, i.e., the filter and rule are called in the same order as the `addFilter()` and `addRule()` methods are listed. JavaScript Validation ===================== -The language of validation rules and conditions is powerful. Even though all constructions work both server-side and client-side, in JavaScript. Rules are transferred in HTML attributes `data-nette-rules` as JSON. -The validation itself is handled by another script, which hooks all form's `submit` events, iterates over all inputs and runs respective validations. +The language for formulating conditions and rules is very powerful. All constructs work both on the server side and on the client side in JavaScript. They are transferred in HTML attributes `data-nette-rules` as JSON. The validation itself is handled by a script that intercepts the form's `submit` event, iterates through the individual controls, and performs the corresponding validation. -This script is `netteForms.js`, which is available from several possible sources: +This script is `netteForms.js`, and it is available from several possible sources: -You can embed the script directly into the HTML page from the CDN: +You can embed the script directly into the HTML page from a CDN: ```latte -<script src="https://nette.github.io/resources/js/3/netteForms.min.js"></script> +<script src="https://unpkg.com/nette-forms@3"></script> ``` -Or copy locally to the public folder of the project (e.g. from `vendor/nette/forms/src/assets/netteForms.min.js`): +Or copy it locally to the public folder of your project (e.g., from `vendor/nette/forms/src/assets/netteForms.min.js`): ```latte <script src="/path/to/netteForms.min.js"></script> ``` -Or install via [npm|https://www.npmjs.com/package/nette-forms]: +Or install it via [npm |https://www.npmjs.com/package/nette-forms]: -```bash +```shell npm install nette-forms ``` -And then load and run: +And then load and run it: ```js import netteForms from 'nette-forms'; netteForms.initOnLoad(); ``` -Alternatively, you can load it directly from the folder `vendor`: +Alternatively, you can load it directly from the `vendor` folder: ```js import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; @@ -314,19 +324,19 @@ netteForms.initOnLoad(); Dynamic JavaScript ================== -Do you only want to show the address fields only if the user chooses to send the goods by post? No problem. The key is a pair of methods `addCondition()` & `toggle()`: +Want to display the address fields only if the user chooses to have the goods sent by post? No problem. The key is the pair of methods `addCondition()` & `toggle()`: ```php $form->addCheckbox('send_it') - ->addCondition($form::EQUAL, true) + ->addCondition($form::Equal, true) ->toggle('#address-container'); ``` -This code says that when the condition is met, that is, when the checkbox is checked, the HTML element `#address-container` will be visible. And vice versa. So, we place the form elements with the recipient's address in a container with that ID, and when the checkbox is clicked, they are hidden or shown. This is handled by the `netteForms.js` script. +This code states that when the condition is met (i.e., when the checkbox is checked), the HTML element `#address-container` will be visible, and vice versa. So, we place the form controls with the recipient's address in a container with this ID, and they will hide or show when the checkbox is clicked. This is handled by the `netteForms.js` script. -Any selector can be passed as an argument to the `toggle()` method. For historical reasons, an alphanumeric string with no other special characters is treated as an element ID, the same as if it were preceded by the `#` character. The second optional parameter allows us to reverse the behavior, i.e. if we used `toggle('#address-container', false)`, the element would be displayed only if the checkbox was unchecked. +Any selector can be passed as an argument to the `toggle()` method. For historical reasons, an alphanumeric string without other special characters is treated as an element ID, just as if it were preceded by the `#` character. The second optional parameter allows reversing the behavior; for instance, if we used `toggle('#address-container', false)`, the element would be displayed only if the checkbox was *not* checked. -The default JavaScript implementation changes the `hidden` property for elements. However, we can easily change the behavior, for example by adding an animation. Just override the `Nette.toggle` method in JavaScript with a custom solution: +The default JavaScript implementation changes the `hidden` property of the elements. However, we can easily change the behavior, for example, by adding an animation. Just override the `Nette.toggle` method in JavaScript with a custom solution: ```js Nette.toggle = (selector, visible, srcElement, event) => { @@ -340,7 +350,7 @@ Nette.toggle = (selector, visible, srcElement, event) => { Disabling Validation ==================== -In certain cases, you need to disable validation. If a submit button isn't supposed to run validation after submitting (for example *Cancel* or *Preview* button), you can disable the validation by calling `$submit->setValidationScope([])`. You can also validate the form partially by specifying items to be validated. +Sometimes it might be useful to disable validation. If pressing a submit button should not perform validation (suitable for *Cancel* or *Preview* buttons), we disable it using the `$submit->setValidationScope([])` method. If it should perform only partial validation, we can specify which fields or form containers should be validated. ```php $form->addText('name') @@ -356,11 +366,11 @@ $form->addSubmit('send1'); // Validates the whole form $form->addSubmit('send2') ->setValidationScope([]); // Validates nothing $form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Validates only 'name' field + ->setValidationScope([$form['name']]); // Validates only the 'name' control $form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Validates only 'age' field + ->setValidationScope([$form['details']['age']]); // Validates only the 'age' control $form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Validates 'details' container + ->setValidationScope([$form['details']]); // Validates the 'details' container ``` -[#Event onValidate] on the form is always invoked and is not affected by the `setValidationScope`. `onValidate` event on the container is invoked only when this container is specified for partial validation. +`setValidationScope` does not affect the [#Event onValidate] on the form, which will always be called. The `onValidate` event on a container will only be triggered if that container is marked for partial validation. diff --git a/forms/meta.json b/forms/meta.json index 504d99a95a..bbce799ffe 100644 --- a/forms/meta.json +++ b/forms/meta.json @@ -1,5 +1,5 @@ { - "version": "4.0", + "version": "3.x", "repo": "nette/forms", "composer": "nette/forms" } diff --git a/http/cs/@left-menu.texy b/http/cs/@left-menu.texy index 7016e6379b..8753fc54a5 100644 --- a/http/cs/@left-menu.texy +++ b/http/cs/@left-menu.texy @@ -1,4 +1,4 @@ -Nette Http +Nette HTTP ********** - [Úvod |@home] - [HTTP request|request] diff --git a/http/cs/@meta.texy b/http/cs/@meta.texy new file mode 100644 index 0000000000..462d9add80 --- /dev/null +++ b/http/cs/@meta.texy @@ -0,0 +1 @@ +{{sitename: Nette Dokumentace}} diff --git a/http/cs/configuration.texy b/http/cs/configuration.texy index d5f8bf5e61..4d9bc137d9 100644 --- a/http/cs/configuration.texy +++ b/http/cs/configuration.texy @@ -72,7 +72,7 @@ http: HTTP cookie ----------- -Lze změnit vychozí hodnoty některých parametrů metody [Nette\Http\Response::setCookie()|response#setCookie] a session. +Lze změnit vychozí hodnoty některých parametrů metody [Nette\Http\Response::setCookie() |response#setCookie] a session. ```neon http: @@ -99,7 +99,7 @@ Výchozí hodnota `auto` u atributu `cookieSecure` znamená, že pokud web běž HTTP proxy ---------- -Pokud web běží za HTTP proxy, zadejte její IP adresu, aby správně fungovala detekce spojení přes HTTPS a také IP adresy klienta. Tedy aby funkce [Nette\Http\Request::getRemoteAddress()|request#getRemoteAddress] a [isSecured()|request#isSecured] vracely správné hodnoty a v šablonách se generovaly odkazy s `https:` protokolem. +Pokud web běží za HTTP proxy, zadejte její IP adresu, aby správně fungovala detekce spojení přes HTTPS a také IP adresy klienta. Tedy aby funkce [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] a [isSecured() |request#isSecured] vracely správné hodnoty a v šablonách se generovaly odkazy s `https:` protokolem. ```neon http: @@ -128,7 +128,7 @@ session: handler: @handlerService ``` -Volba `autoStart` řídí, kdy se má startovat session. Hodnota `always` znamená, že se session spustí vždy se spuštěním aplikace. Hodnota `smart` znamená, že session se spustí při startu aplikace pouze tehdy, pokud již existuje, nebo ve chvíli, z ní chceme číst nebo do ní zapisovat. A nakonec hodnota `never` zakazuje automatický start session. Toto platí od "nette/http verze 3.1.5":[https://github.com/nette/http/releases/tag/v3.1.5]. +Volba `autoStart` řídí, kdy se má startovat session. Hodnota `always` znamená, že se session spustí vždy se spuštěním aplikace. Hodnota `smart` znamená, že session se spustí při startu aplikace pouze tehdy, pokud již existuje, nebo ve chvíli, z ní chceme číst nebo do ní zapisovat. A nakonec hodnota `never` zakazuje automatický start session. Dále lze nastavovat všechny PHP [session direktivy |https://www.php.net/manual/en/session.configuration.php] (ve formátu camelCase) a také [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Příklad: @@ -145,7 +145,7 @@ session: Session cookie -------------- -Session cookie se odesílá se stejnými parametry jako [jiné cookie|#HTTP cookie], ale tyto můžete pro ni změnit: +Session cookie se odesílá se stejnými parametry jako [jiné cookie |#HTTP cookie], ale tyto můžete pro ni změnit: ```neon session: @@ -156,4 +156,16 @@ session: cookieSamesite: None # (Strict|Lax|None) výchozí je Lax ``` -Atribut `cookieSamesite` ovlivňuje, zda bude cookie odeslaná při [přístupu z jiné domény|nette:glossary#SameSite cookie], což poskytuje určitou ochranu před útoky [Cross-Site Request Forgery |nette:glossary#cross-site-request-forgery-csrf] (CSRF). +Atribut `cookieSamesite` ovlivňuje, zda bude cookie odeslaná při [přístupu z jiné domény |nette:glossary#SameSite cookie], což poskytuje určitou ochranu před útoky [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). + + +Služby DI +========= + +Tyto služby se přidávají do DI kontejneru: + +| Název | Typ | Popis +|----------------------------------------------------- +| `http.request` | [api:Nette\Http\Request] | [HTTP request| request] +| `http.response` | [api:Nette\Http\Response] | [HTTP response| response] +| `session.session` | [api:Nette\Http\Session] | [správa session| sessions] diff --git a/http/cs/request.texy b/http/cs/request.texy index cc8094cccf..5dc90de308 100644 --- a/http/cs/request.texy +++ b/http/cs/request.texy @@ -4,9 +4,9 @@ HTTP request .[perex] Nette zapouzdřuje HTTP požadavek do objektů se srozumitelným API a zároveň poskytuje sanitizační filtr. -HTTP požadavek představuje objekt [api:Nette\Http\Request], ke kterému se dostanete tak, že si jej necháte předat pomocí [dependency injection |dependency-injection:passing-dependencies]. V presenterech stačí jen zavolat `$httpRequest = $this->getHttpRequest()`. +HTTP požadavek představuje objekt [api:Nette\Http\Request]. Pokud pracujete s Nette, tento objekt je automaticky vytvořen frameworkem a můžete si jej nechat předat pomocí [dependency injection |dependency-injection:passing-dependencies]. V presenterech stačí jen zavolat metodu `$this->getHttpRequest()`. Pokud pracujete mimo Nette Framework, můžete si vytvořit objekt pomocí [#RequestFactory]. -Co je důležité, tak že Nette když [vytváří|#RequestFactory] tento objekt, všechny vstupní parametry GET, POST, COOKIE a také URL pročistí od kontrolních znaků a neplatných UTF-8 sekvencí. Takže s daty pak můžete bezpečně dále pracovat. Očištěná data se následně používají v presenterech a formulářích. +Velkou předností Nette je, že při vytváření objektu automaticky pročišťuje všechny vstupní parametry GET, POST, COOKIE a také URL od kontrolních znaků a neplatných UTF-8 sekvencí. S těmito daty pak můžete bezpečně dále pracovat. Očištěná data se následně používají v presenterech a formulářích. → [Instalace a požadavky |@home#Instalace] @@ -24,7 +24,7 @@ Vrací klon s jinou URL. getUrl(): Nette\Http\UrlScript .[method] ---------------------------------------- -Vrací URL požadavku jako objekt [UrlScript|urls#UrlScript]. +Vrací URL požadavku jako objekt [UrlScript |urls#UrlScript]. ```php $url = $httpRequest->getUrl(); @@ -32,11 +32,11 @@ echo $url; // https://doc.nette.org/cs/?action=edit echo $url->getHost(); // nette.org ``` -Prohlížeče neodesílají na server fragment, takže `$url->getFragment()` bude vracet prázdný řetězec. +Upozornění: prohlížeče neodesílají na server fragment, takže `$url->getFragment()` bude vracet prázdný řetězec. -getQuery(string $key=null): string|array|null .[method] -------------------------------------------------------- +getQuery(?string $key=null): string|array|null .[method] +-------------------------------------------------------- Vrací parametry GET požadavku. ```php @@ -45,8 +45,8 @@ $id = $httpRequest->getQuery('id'); // vrací GET parametr 'id' (nebo null) ``` -getPost(string $key=null): string|array|null .[method] ------------------------------------------------------- +getPost(?string $key=null): string|array|null .[method] +------------------------------------------------------- Vrací parametry POST požadavku. ```php @@ -57,11 +57,11 @@ $id = $httpRequest->getPost('id'); // vrací POST parametr 'id' (nebo null) getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] ------------------------------------------------------------------------- -Vrací [upload|#Uploadované soubory] jako objekt [api:Nette\Http\FileUpload]: +Vrací [upload |#Uploadované soubory] jako objekt [api:Nette\Http\FileUpload]: ```php $file = $httpRequest->getFile('avatar'); -if ($file->hasFile()) { // byl nějaký soubor nahraný? +if ($file?->hasFile()) { // byl nějaký soubor nahraný? $file->getUntrustedName(); // jméno souboru odeslané uživatelem $file->getSanitizedName(); // jméno bez nebezpečných znaků } @@ -79,7 +79,7 @@ Protože nelze důvěřovat datům zvenčí a tedy ani spoléhat na podobu struk getFiles(): array .[method] --------------------------- -Vrátí strom [všech uploadů|#Uploadované soubory] v normalizované struktuře, jejíž listy jsou objekty [api:Nette\Http\FileUpload]: +Vrátí strom [všech uploadů |#Uploadované soubory] v normalizované struktuře, jejíž listy jsou objekty [api:Nette\Http\FileUpload]: ```php $files = $httpRequest->getFiles(); @@ -141,14 +141,9 @@ echo $headers['Content-Type']; ``` -getReferer(): ?Nette\Http\UrlImmutable .[method] ------------------------------------------------- -Z jaké URL uživatel přišel? Pozor, není vůbec spolehlivé. - - isSecured(): bool .[method] --------------------------- -Je spojení šifrované (HTTPS)? Pro správnou funkčnost může být potřeba [nastavit proxy|configuration#HTTP proxy]. +Je spojení šifrované (HTTPS)? Pro správnou funkčnost může být potřeba [nastavit proxy |configuration#HTTP proxy]. isSameSite(): bool .[method] @@ -163,16 +158,16 @@ Jde o AJAXový požadavek? getRemoteAddress(): ?string .[method] ------------------------------------- -Vrací IP adresu uživatele. Pro správnou funkčnost může být potřeba [nastavit proxy|configuration#HTTP proxy]. +Vrací IP adresu uživatele. Pro správnou funkčnost může být potřeba [nastavit proxy |configuration#HTTP proxy]. getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Vrací DNS překlad IP adresy uživatele. Pro správnou funkčnost může být potřeba [nastavit proxy|configuration#HTTP proxy]. +Vrací DNS překlad IP adresy uživatele. Pro správnou funkčnost může být potřeba [nastavit proxy |configuration#HTTP proxy]. -getBasicCredentials(): ?string .[method]{data-version:v3.2} ------------------------------------------------------------ +getBasicCredentials(): ?array .[method] +--------------------------------------- Vrací ověřovací údaje pro [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. ```php @@ -204,28 +199,35 @@ echo $httpRequest->detectLanguage($langs); // en RequestFactory ============== -Objekt aktuálního HTTP requestu vyrobí [api:Nette\Http\RequestFactory]. Pokud píšete aplikaci, která nepoužívá DI kontejner, vyrobíte request takto: +Třída [api:Nette\Http\RequestFactory] slouží k vytvoření instance `Nette\Http\Request`, která reprezentuje aktuální HTTP požadavek. (Pokud pracujete s Nette, objekt HTTP požadavku je automaticky vytvořen frameworkem.) ```php $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -RequestFactory lze před zavoláním `fromGlobals()` konfigurovat. Můžeme vypnout sanitizaci vstupních parametrů od kontrolních znaků a neplatných UTF-8 sekvencí pomocí `$factory->setBinary()`. A také nastavit proxy server pomocí `$factory->setProxy(...)`, což je důležité pro správnou detekci IP adresy uživatele. +Metoda `fromGlobals()` vytvoří objekt požadavku na základě aktuálních globálních proměnných PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` a `$_SERVER`). Při vytváření objektu automaticky pročišťuje všechny vstupní parametry GET, POST, COOKIE a také URL od kontrolních znaků a neplatných UTF-8 sekvencí, což zajišťuje bezpečnost při další práci s těmito daty. -Pomocí tzv. filtrů lze URL vyčistit od znaků, které se do něj mohou dostat např. kvůli špatně implementovaným komentářovým systémům na různých cizích webech: +RequestFactory lze před zavoláním `fromGlobals()` konfigurovat: + +- metodou `$factory->setBinary()` vypnete automatické čištění vstupních parametrů od kontrolních znaků a neplatných UTF-8 sekvencí. +- metodou `$factory->setProxy(...)` uvedete IP adresu [proxy serveru |configuration#HTTP proxy], což je nezbytné pro správnou detekci IP adresy uživatele. + +RequestFactory umožňuje definovat filtry, které automaticky transformují části URL požadavku. Tyto filtry odstraňují nežádoucí znaky z URL, které tam mohou být vloženy například nesprávnou implementací komentářových systémů na různých webech: ```php -// odstraníme mezery z cesty +// odstranění mezer z cesty $requestFactory->urlFilters['path']['%20'] = ''; -// odstraníme tečku, čárku nebo pravou závorku z konce URI +// odstranění tečky, čárky nebo pravé závorky z konce URI $requestFactory->urlFilters['url']['[.,)]$'] = ''; -// vyčistíme cestu od zdvojených lomítek (výchozí filtr) +// vyčištění cesty od zdvojených lomítek (výchozí filtr) $requestFactory->urlFilters['path']['/{2,}'] = '/'; ``` +První klíč `'path'` nebo `'url'` určuje, na kterou část URL se filtr použije. Druhý klíč je regulární výraz, který se má vyhledat, a hodnota je náhrada, která se použije místo nalezeného textu. + Uploadované soubory =================== @@ -249,7 +251,7 @@ V tomto případě `$request->getFiles()` vrací pole: Objekt `FileUpload` se vytvoří i v případě, že uživatel žádný soubor neodeslal nebo odeslání selhalo. Jestli byl soubor odeslán vrací metoda `hasFile()`: ```php -$request->getFile('avatar')->hasFile(); +$request->getFile('avatar')?->hasFile(); ``` V případě názvu elementu používajícího notaci pro pole: @@ -347,7 +349,7 @@ Vyžaduje PHP rozšíření `fileinfo`. getUntrustedName(): string .[method] ------------------------------------ -Vrací originální název souboru, jak jej odeslal prohlížeč. V nette/http 3.0 a starších se metoda jmenovala `getName()`. +Vrací originální název souboru, jak jej odeslal prohlížeč. .[caution] Nevěřte hodnotě vrácené touto metodou. Klient mohl odeslat škodlivý název souboru s úmyslem poškodit nebo hacknout vaši aplikaci. @@ -355,11 +357,22 @@ Nevěřte hodnotě vrácené touto metodou. Klient mohl odeslat škodlivý náze getSanitizedName(): string .[method] ------------------------------------ -Vrací sanitizovaný název souboru. Obsahuje pouze ASCII znaky `[a-zA-Z0-9.-]`. Pokud název takové znaky neobsahuje, vrátí `'unknown'`. Pokud je soubor obrázek ve formátu JPEG, PNG, GIF, nebo WebP, vrátí i správnou příponu. +Vrací sanitizovaný název souboru. Obsahuje pouze ASCII znaky `[a-zA-Z0-9.-]`. Pokud název takové znaky neobsahuje, vrátí `'unknown'`. Pokud je soubor obrázek ve formátu JPEG, PNG, GIF, WebP nebo AVIF, vrátí i správnou příponu. + +.[caution] +Vyžaduje PHP rozšíření `fileinfo`. + + +getSuggestedExtension(): ?string .[method]{data-version:3.2.4} +-------------------------------------------------------------- +Vrací vhodnou příponu souboru (bez tečky) odpovídající zjištěnému MIME typu. +.[caution] +Vyžaduje PHP rozšíření `fileinfo`. -getUntrustedFullPath(): string .[method]{data-version:v3.2} ------------------------------------------------------------ + +getUntrustedFullPath(): string .[method] +---------------------------------------- Vrací originální cestu k souboru, jak ji odeslal prohlížeč při uploadu složky. Celá cesta je dostupná pouze v PHP 8.1 a vyšším. V předchozích verzích tato metoda vrací originální název souboru. .[caution] @@ -378,7 +391,7 @@ Vrací cestu k dočasné lokaci uploadovaného souboru. V případě, že upload isImage(): bool .[method] ------------------------- -Vrací `true`, pokud nahraný soubor je obrázek ve formátu JPEG, PNG, GIF, nebo WebP. Detekce probíhá na základě jeho signatury a neověřuje se integrita celého souboru. Zda není obrázek poškozený lze zjistit například pokusem o jeho [načtení|#toImage]. +Vrací `true`, pokud nahraný soubor je obrázek ve formátu JPEG, PNG, GIF, WebP nebo AVIF. Detekce probíhá na základě jeho signatury a neověřuje se integrita celého souboru. Zda není obrázek poškozený lze zjistit například pokusem o jeho [načtení |#toImage]. .[caution] Vyžaduje PHP rozšíření `fileinfo`. diff --git a/http/cs/response.texy b/http/cs/response.texy index 9b3f2a2d50..baea11f900 100644 --- a/http/cs/response.texy +++ b/http/cs/response.texy @@ -4,7 +4,7 @@ HTTP response .[perex] Nette zapouzdřuje HTTP odpověď do objektů se srozumitelným API. -HTTP odpověď představuje objekt [api:Nette\Http\Response], ke kterému se dostanete tak, že si jej necháte předat pomocí [dependency injection |dependency-injection:passing-dependencies]. V presenterech stačí jen zavolat `$httpResponse = $this->getHttpResponse()`. +HTTP odpověď představuje objekt [api:Nette\Http\Response]. Pokud pracujete s Nette, tento objekt je automaticky vytvořen frameworkem a můžete si jej nechat předat pomocí [dependency injection |dependency-injection:passing-dependencies]. V presenterech stačí jen zavolat metodu `$this->getHttpResponse()`. → [Instalace a požadavky |@home#Instalace] @@ -15,12 +15,12 @@ Nette\Http\Response Objekt je na rozdíl od [Nette\Http\Request|request] mutable, tedy pomocí setterů můžete měnit stav, tedy např. odesílat hlavičky. Nezapomeňte, že všechny settery musí být volány **před odesláním jakéhokoli výstupu.** Jestli už byl výstup odeslán prozradí metoda `isSent()`. Pokud vrací `true`, každý pokus o odeslání hlavičky vyvolá výjimku `Nette\InvalidStateException`. -setCode(int $code, string $reason=null) .[method] -------------------------------------------------- +setCode(int $code, ?string $reason=null) .[method] +-------------------------------------------------- Změní stavový [kód odpovědi |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Kvůli lepší srozumitelnosti zdrojového kódu doporučujeme pro kód používat místo čísel [předdefinované konstanty |api:Nette\Http\IResponse]. ```php -$httpResponse->setCode(Nette\Http\Response::S404_NOT_FOUND); +$httpResponse->setCode(Nette\Http\Response::S404_NotFound); ``` @@ -77,8 +77,8 @@ echo $headers['Pragma']; ``` -setContentType(string $type, string $charset=null) .[method] ------------------------------------------------------------- +setContentType(string $type, ?string $charset=null) .[method] +------------------------------------------------------------- Mění hlavičku `Content-Type`. ```php @@ -86,7 +86,7 @@ $httpResponse->setContentType('text/plain', 'UTF-8'); ``` -redirect(string $url, int $code=self::S302_FOUND): void .[method] +redirect(string $url, int $code=self::S302_Found): void .[method] ----------------------------------------------------------------- Přesměruje na jiné URL. Nezapomeňte poté ukončit skript. @@ -115,17 +115,17 @@ $httpResponse->sendAsFile('faktura.pdf'); ``` -setCookie(string $name, string $value, $time, string $path=null, string $domain=null, bool $secure=null, bool $httpOnly=null, string $sameSite=null) .[method] --------------------------------------------------------------------------------------------------------------------------------------------------------------- +setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------- Odešle cookie. Výchozí hodnoty parametrů: | `$path` | `'/'` | cookie má dosah na všechny cesty v (sub)doméně *(konfigurovatelné)* | `$domain` | `null` | což znamená s dosahem na aktuální (sub)doménu, ale nikoliv její subdomény *(konfigurovatelné)* | `$secure` | `true` | pokud web běží na HTTPS, jinak `false` *(konfigurovatelné)* | `$httpOnly` | `true` | cookie je pro JavaScript nepřístupná -| `$sameSite` | `'Lax'` | cookie nemusí být odeslána při [přístupu z jiné domény|nette:glossary#SameSite cookie] +| `$sameSite` | `'Lax'` | cookie nemusí být odeslána při [přístupu z jiné domény |nette:glossary#SameSite cookie] -Výchozí hodnoty parametrů `$path`, `$domain` a `$secure` můžete změnit v [konfiguraci|configuration#HTTP cookie]. +Výchozí hodnoty parametrů `$path`, `$domain` a `$secure` můžete změnit v [konfiguraci |configuration#HTTP cookie]. Čas lze uvádět jako počet sekund nebo řetězec: @@ -135,15 +135,15 @@ $httpResponse->setCookie('lang', 'cs', '100 days'); Parametr `$domain` určuje, které domény mohou cookie přijímat. Není-li uveden, cookie přijímá stejná (sub)doména, jako ji nastavila, ale nikoliv její subdomény. Pokud je `$domain` zadaný, jsou zahrnuty i subdomény. Proto je uvedení `$domain` méně omezující než vynechání. Například při `$domain = 'nette.org'` jsou cookies dostupné i na všech subdoménách jako `doc.nette.org`. -Pro hodnotu `$sameSite` můžete použít konstanty `Response::SAME_SITE_LAX`, `SAME_SITE_STRICT` a `SAME_SITE_NONE`. +Pro hodnotu `$sameSite` můžete použít konstanty `Response::SameSiteLax`, `SameSiteStrict` a `SameSiteNone`. -deleteCookie(string $name, string $path=null, string $domain=null, bool $secure=null): void .[method] ------------------------------------------------------------------------------------------------------ +deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] +-------------------------------------------------------------------------------------------------------- Smaže cookie. Výchozí hodnoty parametrů jsou: - `$path` s dosahem na všechny adresáře (`'/'`) - `$domain` s dosahem na aktuální (sub)doménu, ale nikoliv její subdomény -- `$secure` se řídí podle nastavení v [konfiguraci|configuration#HTTP cookie] +- `$secure` se řídí podle nastavení v [konfiguraci |configuration#HTTP cookie] ```php $httpResponse->deleteCookie('lang'); diff --git a/http/cs/sessions.texy b/http/cs/sessions.texy index 29e9999937..f4ef752aec 100644 --- a/http/cs/sessions.texy +++ b/http/cs/sessions.texy @@ -13,7 +13,7 @@ HTTP je bezestavový protokol, nicméně takřka každá aplikace potřebuje sta Při použití sessions každý uživatel obdrží jedinečný identifikátor nazývaný session ID, který se předává v cookie. Ten slouží jako klíč k session datům. Na rozdíl od cookies, které se uchovávají na straně prohlížeče, jsou data v session uchovávána na straně serveru. -Session nastavujeme v [konfiguraci |configuration#session], důležitá je zejména volba doby exipirace. +Session nastavujeme v [konfiguraci |configuration#Session], důležitá je zejména volba doby exipirace. Správu session má na starosti objekt [api:Nette\Http\Session], ke kterému se dostanete tak, že si jej necháte předat pomocí [dependency injection |dependency-injection:passing-dependencies]. V presenterech stačí jen zavolat `$session = $this->getSession()`. @@ -52,7 +52,7 @@ $section = $this->getSession('unikatni nazev'); Ověřit existenci sekce lze metodou `$session->hasSection('unikatni nazev')`. -Se samotnou sekcí se pak pracuje velmi snadno pomocí metod `set()`, `get()` a `remove()`: (od "nette/http v3.1.5":[https://github.com/nette/http/releases/tag/v3.1.5]) +Se samotnou sekcí se pak pracuje velmi snadno pomocí metod `set()`, `get()` a `remove()`: ```php // zápis proměnné @@ -65,16 +65,6 @@ echo $section->get('userName'); $section->remove('userName'); ``` -Před verzí 3.1.5 se používaly magické proměnné nebo ArrayAccess, které budou ve verzi 4 označeny za zastaralé. - -```php -$section->userName = 'franta'; // nebo $section['userName'] = 'franta'; - -echo $section->userName; // nebo echo $section['userName']; - -unset($section->userName); // unset($section['userName']); -``` - Pro získání všech proměnných ze sekce je možné použít cyklus `foreach`: ```php @@ -94,29 +84,23 @@ Pro jednotlivé sekce nebo dokonce jednotlivé proměnné je možné nastavit ex $section->setExpiration('20 minutes'); ``` -Pro nastavení expirace jednotlivých proměnných slouží třetí parametr metody `set()`: (od "nette/http v3.1.5":[https://github.com/nette/http/releases/tag/v3.1.5]) +Pro nastavení expirace jednotlivých proměnných slouží třetí parametr metody `set()`: ```php // proměnná 'flash' vyexpiruje už po 30 sekundách $section->set('flash', $message, '30 seconds'); ``` -Před verzí 3.1.5 se použivala metoda `setExpiration()`: - -```php -$section->setExpiration('30 seconds', 'flash'); -``` - .[note] -Nezapomeňte, že doba expirace celé session (viz [konfigurace session|configuration#session]) musí být stejná nebo vyšší než doba nastavená u jednotlivých sekcí či proměnných. +Nezapomeňte, že doba expirace celé session (viz [konfigurace session |configuration#Session]) musí být stejná nebo vyšší než doba nastavená u jednotlivých sekcí či proměnných. Zrušení dříve nastavené expirace docílíme metodou `removeExpiration()`. Okamžité zrušení celé sekce zajistí metoda `remove()`. -Události $onStart, $onBeforeWrite .{data-version:v3.1.5} --------------------------------------------------------- +Události $onStart, $onBeforeWrite +--------------------------------- -Objekt `Nette\Http\Session` má [události|nette:glossary#Události] `$onStart` a `$onBeforeWrite`, můžete tedy přidat callbacky, které se vyvolají po startu session nebo před jejím zápisem na disk a následným ukončením. +Objekt `Nette\Http\Session` má [události |nette:glossary#události] `$onStart` a `$onBeforeWrite`, můžete tedy přidat callbacky, které se vyvolají po startu session nebo před jejím zápisem na disk a následným ukončením. ```php $session->onBeforeWrite[] = function () { @@ -174,7 +158,7 @@ Vrátí session ID. Konfigurace ----------- -Session nastavujeme v [konfiguraci |configuration#session]. Pokud píšete aplikaci, která nepoužívá DI kontejner, slouží ke konfiguraci tyto metody. Musí být volány ještě před spuštěním session. +Session nastavujeme v [konfiguraci |configuration#Session]. Pokud píšete aplikaci, která nepoužívá DI kontejner, slouží ke konfiguraci tyto metody. Musí být volány ještě před spuštěním session. <div class=wiki-methods-brief> @@ -199,9 +183,9 @@ setExpiration(?string $time): static .[method] Nastaví dobu neaktivity po které session vyexpiruje. -setCookieParameters(string $path, string $domain=null, bool $secure=null, string $samesite=null): static .[method] ------------------------------------------------------------------------------------------------------------------- -Nastavení parametrů pro cookie. Výchozí hodnoty parametrů můžete změnit v [konfiguraci|configuration#Session cookie]. +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] +--------------------------------------------------------------------------------------------------------------------- +Nastavení parametrů pro cookie. Výchozí hodnoty parametrů můžete změnit v [konfiguraci |configuration#Session cookie]. setSavePath(string $path): static .[method] @@ -223,4 +207,5 @@ Server předpokládá, že komunikuje stále s tímtéž uživatelem, dokud pož Nette Framework proto správně nakonfiguruje PHP direktivy, aby session ID přenášel pouze v cookie, znepřístupnil jej JavaScriptu a případné identifikátory v URL ignoroval. Navíc v kritických chvílích, jako je třeba přihlášení uživatele, vygeneruje session ID nové. -Pro konfiguraci PHP se používá funkce ini_set, kterou bohužel některé hostingy zakazují. Pokud je to případ i vašeho hostéra, pokuste se s ním domluvit, aby vám funkci povolil nebo alespoň server nakonfiguroval. .[note] +.[note] +Pro konfiguraci PHP se používá funkce ini_set, kterou bohužel některé hostingy zakazují. Pokud je to případ i vašeho hostéra, pokuste se s ním domluvit, aby vám funkci povolil nebo alespoň server nakonfiguroval. diff --git a/http/cs/urls.texy b/http/cs/urls.texy index 71f7a0fa1e..9e722a9d7c 100644 --- a/http/cs/urls.texy +++ b/http/cs/urls.texy @@ -1,5 +1,5 @@ -Parsování a skládání URL -************************ +Práce s URL +*********** .[perex] Třídy [#Url], [#UrlImmutable] a [#UrlScript] umožňují snadné generování, parsování a manipulaci s URL. @@ -40,10 +40,21 @@ Lze také URL naparsovat a dále s ním manipulovat: ```php $url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer' + 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', ); ``` +Třída `Url` implementuje rozhraní `JsonSerializable` a má metodu `__toString()`, takže objekt lze vypsat nebo použít v datech předávaných do `json_encode()`. + +```php +echo $url; +echo json_encode([$url]); +``` + + +Komponenty URL .[method] +------------------------ + Pro vrácení nebo změnu jednotlivých komponent URL jsou vám k dispozici tyto metody: .[language-php] @@ -58,10 +69,12 @@ Pro vrácení nebo změnu jednotlivých komponent URL jsou vám k dispozici tyto | `setPath(string $path)` | `getPath(): string` | `'/en/download'` | `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` | `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'nette.org:8080'` -| | `getHostUrl(): string` | `'http://nette.org:8080'` +| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` +| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` | | `getAbsoluteUrl(): string` | celá URL +Upozornění: Když pracujete s URL, které je získáno z [HTTP requestu|request], mějte na paměti, že nebude obsahovat fragment, protože prohlížeč jej neodesílá na server. + Můžeme pracovat i s jednotlivými query parametry pomocí: .[language-php] @@ -70,7 +83,10 @@ Můžeme pracovat i s jednotlivými query parametry pomocí: | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` -Metoda `getDomain(int $level = 2)` vrací pravou či levou část hostitele. Takto funguje, pokud host je `www.nette.org`: + +getDomain(int $level = 2): string .[method] +------------------------------------------- +Vrací pravou či levou část hostitele. Takto funguje, pokud host je `www.nette.org`: .[language-php] | `getDomain(1)` | `'org'` @@ -82,30 +98,46 @@ Metoda `getDomain(int $level = 2)` vrací pravou či levou část hostitele. Tak | `getDomain(-3)` | `''` -Třída `Url` implementuje rozhraní `JsonSerializable` a má metodu `__toString()`, takže objekt lze vypsat nebo použít v datech předávaných do `json_encode()`. +isEqual(string|Url $anotherUrl): bool .[method] +----------------------------------------------- +Ověří, zda jsou dvě URL shodné. ```php -echo $url; -echo json_encode([$url]); +$url->isEqual('https://nette.org'); ``` -Metoda `isEqual(string|Url $anotherUrl): bool` ověří, zda jsou dvě URL shodné. + +Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} +---------------------------------------------------------------- +Ověřuje, zda je URL absolutní. URL je považována za absolutní, pokud začíná schématem (např. http, https, ftp) následovaným dvojtečkou. ```php -$url->isEqual('https://nette.org'); +Url::isAbsolute('https://nette.org'); // true +Url::isAbsolute('//nette.org'); // false +``` + + +Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} +-------------------------------------------------------------------------- +Normalizuje cestu v URL odstraněním speciálních segmentů `.` a `..`. Metoda odstraňuje nadbytečné prvky cesty stejným způsobem, jako to dělají webové prohlížeče. + +```php +Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' +Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' +Url::removeDotSegments('./today/../file.txt'); // 'file.txt' ``` UrlImmutable ============ -Třída [api:Nette\Http\UrlImmutable] je immutable (neměnnou) alternativou třídy `Url` (podobně jako je v PHP `DateTimeImmutable` neměnnou alternativou `DateTime`). Místo setterů má tzv. withery, které objekt nemění, ale vracejí nové instance s upravenou hodnotou: +Třída [api:Nette\Http\UrlImmutable] je immutable (neměnnou) alternativou třídy [#Url] (podobně jako je v PHP `DateTimeImmutable` neměnnou alternativou `DateTime`). Místo setterů má tzv. withery, které objekt nemění, ale vracejí nové instance s upravenou hodnotou: ```php use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer' + 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', ); $newUrl = $url @@ -113,9 +145,20 @@ $newUrl = $url ->withPassword('') ->withPath('/cs/'); -echo $newUrl; // 'http://nette.org:8080/cs/?name=param#footer' +echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' ``` +Třída `UrlImmutable` implementuje rozhraní `JsonSerializable` a má metodu `__toString()`, takže objekt lze vypsat nebo použít v datech předávaných do `json_encode()`. + +```php +echo $url; +echo json_encode([$url]); +``` + + +Komponenty URL .[method] +------------------------ + Pro vrácení nebo změnu jednotlivých komponent URL slouží metody: .[language-php] @@ -130,10 +173,12 @@ Pro vrácení nebo změnu jednotlivých komponent URL slouží metody: | `withPath(string $path)` | `getPath(): string` | `'/en/download'` | `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` | `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'nette.org:8080'` -| | `getHostUrl(): string` | `'http://nette.org:8080'` +| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` +| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` | | `getAbsoluteUrl(): string` | celá URL +Metoda `withoutUserInfo()` odstraňuje `user` a `password`. + Můžeme pracovat i s jednotlivými query parametry pomocí: .[language-php] @@ -142,22 +187,52 @@ Můžeme pracovat i s jednotlivými query parametry pomocí: | `withQuery(string\|array $query)` | `getQueryParameters(): array` | `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` -Metoda `getDomain(int $level = 2)` funguje stejně, jako její jmenovkyně ze třídy `Url`. Metoda `withoutUserInfo()` odstraňuje `user` a `password`. -Třída `UrlImmutable` implementuje rozhraní `JsonSerializable` a má metodu `__toString()`, takže objekt lze vypsat nebo použít v datech předávaných do `json_encode()`. +getDomain(int $level = 2): string .[method] +------------------------------------------- +Vrací pravou či levou část hostitele. Takto funguje, pokud host je `www.nette.org`: + +.[language-php] +| `getDomain(1)` | `'org'` +| `getDomain(2)` | `'nette.org'` +| `getDomain(3)` | `'www.nette.org'` +| `getDomain(0)` | `'www.nette.org'` +| `getDomain(-1)` | `'www.nette'` +| `getDomain(-2)` | `'www'` +| `getDomain(-3)` | `''` + + +resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} +---------------------------------------------------------------------- +Odvozuje absolutní URL stejným způsobem, jakým prohlížeč zpracovává odkazy na HTML stránce: +- pokud je odkaz absolutní URL (obsahuje schéma), použije se beze změny +- pokud odkaz začíná `//`, převezme se pouze schéma z aktuální URL +- pokud odkaz začíná `/`, vytvoří se absolutní cesta od kořene domény +- v ostatních případech se URL sestaví relativně vůči aktuální cestě ```php -echo $url; -echo json_encode([$url]); +$url = new UrlImmutable('https://example.com/path/page'); +echo $url->resolve('../foo'); // 'https://example.com/foo' +echo $url->resolve('/bar'); // 'https://example.com/bar' +echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.html' ``` -Metoda `isEqual(string|Url $anotherUrl): bool` ověří, zda jsou dvě URL shodné. + +isEqual(string|Url $anotherUrl): bool .[method] +----------------------------------------------- +Ověří, zda jsou dvě URL shodné. + +```php +$url->isEqual('https://nette.org'); +``` UrlScript ========= -Třída [api:Nette\Http\UrlScript] je potomkem `UrlImmutable` a navíc rozlišuje tyto další logické části URL: +Třída [api:Nette\Http\UrlScript] je potomkem [#UrlImmutable] a rozšiřuje jej o další virtuální komponenty URL, jako je kořenový adresáři projektu apod. Stejně jako rodičovská třída je immutable (neměnným) objektem. + +Následující diagram zobrazuje komponenty, které UrlScript rozpoznává: /--pre baseUrl basePath relativePath relativeUrl @@ -169,6 +244,13 @@ Třída [api:Nette\Http\UrlScript] je potomkem `UrlImmutable` a navíc rozlišuj scriptPath pathInfo \-- +- `baseUrl` je základní URL adresa aplikace včetně domény a části cesty ke kořenovému adresáři aplikace +- `basePath` je část cesty ke kořenovému adresáři aplikace +- `scriptPath` je cesta k aktuálnímu skriptu +- `relativePath` je název skriptu (případně další segmenty cesty) relativní k basePath +- `relativeUrl` je celá část URL za baseUrl, včetně query string a fragmentu. +- `pathInfo` dnes už málo využívaná část URL za názvem skriptu + Pro vrácení částí URL jsou k dispozici metody: .[language-php] @@ -181,4 +263,4 @@ Pro vrácení částí URL jsou k dispozici metody: | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` -Objekty `UrlScript` přímo nevytváříme, ale vrací jej metoda [Nette\Http\Request::getUrl()|request]. +Objekty `UrlScript` obvykle přímo nevytváříme, ale vrací jej metoda [Nette\Http\Request::getUrl()|request] s již správně nastavenými komponentami pro aktuální HTTP požadavek. diff --git a/http/en/@left-menu.texy b/http/en/@left-menu.texy index 5f72f7dd2e..652706062b 100644 --- a/http/en/@left-menu.texy +++ b/http/en/@left-menu.texy @@ -1,8 +1,8 @@ Nette HTTP ********** - [Overview |@home] -- [HTTP request |request] -- [HTTP response |response] +- [HTTP Request |request] +- [HTTP Response |response] - [Sessions] -- [URL utility |urls] +- [URL Utility |urls] - [Configuration] diff --git a/http/en/@meta.texy b/http/en/@meta.texy new file mode 100644 index 0000000000..42471908b0 --- /dev/null +++ b/http/en/@meta.texy @@ -0,0 +1 @@ +{{sitename: Nette Documentation}} diff --git a/http/en/configuration.texy b/http/en/configuration.texy index ffff4714a1..64ce01eb2d 100644 --- a/http/en/configuration.texy +++ b/http/en/configuration.texy @@ -1,10 +1,10 @@ -Configuring HTTP -**************** +HTTP Configuration +****************** .[perex] -Overview of configuration options for the Nette HTTP. +Overview of configuration options for Nette HTTP. -If you are not using the whole framework, but only this library, read [how to load the configuration|bootstrap:]. +If you are not using the entire framework, but only this library, read [how to load the configuration|bootstrap:]. HTTP Headers @@ -18,23 +18,23 @@ http: X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' - # affects header X-Frame-Options + # affects the X-Frame-Options header frames: ... # (string|bool) defaults to 'SAMEORIGIN' ``` -For security reasons, the framework sends a header `X-Frame-Options: SAMEORIGIN`, which says that a page can be displayed inside another page (in element `<iframe>`) only if it is on the same domain. This can be unwanted in certain situations (for example, if you are developing a Facebook application), so the behavior can be changed by setting `frames: http://allowed-host.com` or `frames: true`. +For security reasons, the framework sends the `X-Frame-Options: SAMEORIGIN` header, which indicates that a page can be displayed inside another page (in an `<iframe>` element) only if it is on the same domain. This might be undesirable in certain situations (e.g., if you are developing a Facebook application), so the behavior can be changed by setting `frames: http://allowed-host.com` or `frames: true`. Content Security Policy ----------------------- -Headers `Content-Security-Policy` (hereinafter referred to as CSP) can be easily assembled, their description can be found in [CSP description |https://content-security-policy.com]. CSP directives (such as `script-src`) can be written either as strings according to specification or as arrays of values ​​for better readability. Then there is no need to write quotation marks around keywords such as `'self'`. Nette will also automatically generate a value of `nonce`, so `'nonce-y4PopTLM=='` will be send in the header. +Headers `Content-Security-Policy` (CSP) can be easily configured; their description can be found in the [CSP specification |https://content-security-policy.com]. CSP directives (such as `script-src`) can be written either as strings according to the specification or as arrays of values for better readability. Then there is no need to use quotation marks around keywords like `'self'`. Nette will also automatically generate a `nonce` value, so something like `'nonce-y4PopTLM=='` will be sent in the header. ```neon http: # Content Security Policy csp: - # string according to CSP specification + # string according to the CSP specification default-src: "'self' https://example.com" # array of values @@ -49,9 +49,9 @@ http: block-all-mixed-content: false ``` -Use `<script n:nonce>...</script>` in the templates and the nonce value will be filled in automatically. Making secure websites in Nette is really easy. +Use `<script n:nonce>...</script>` in templates, and the nonce value will be filled in automatically. Making secure websites in Nette is really easy. -Similarly, headers `Content-Security-Policy-Report-Only` (which can be used in parallel with CSP) and [Feature Policy |https://developers.google.com/web/updates/2018/06/feature-policy] can be added: +Similarly, `Content-Security-Policy-Report-Only` headers (which can be used concurrently with CSP) and [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy] can be configured: ```neon http: @@ -72,39 +72,39 @@ http: HTTP Cookie ----------- -You can change the default values of some parameters of the [Nette\Http\Response::setCookie() |response#setCookie] and session methods. +You can change the default values of some parameters of the [Nette\Http\Response::setCookie() |response#setCookie] method and session handling. ```neon http: # cookie scope by path cookiePath: ... # (string) defaults to '/' - # which hosts are allowed to receive the cookie + # domains that can receive the cookie cookieDomain: 'example.com' # (string|domain) defaults to unset - # to send cookies only via HTTPS? + # send cookies only via HTTPS? cookieSecure: ... # (bool|auto) defaults to auto - # disables the sending of the cookie that Nette uses as protection against CSRF + # disables sending the cookie that Nette uses for CSRF protection disableNetteCookie: ... # (bool) defaults to false ``` -The `cookieDomain` option determines which domains (origins) can accept cookies. If not specified, the cookie is accepted by the same (sub)domain as is set by it, *excluding* their subdomains. If `cookieDomain` is specified, then subdomains are also included. Therefore, specifying `cookieDomain` is less restrictive than omitting. +The `cookieDomain` attribute determines which domains (origins) can accept cookies. If not specified, the cookie is accepted by the same (sub)domain that set it, *excluding* its subdomains. If `cookieDomain` is specified, subdomains are also included. Therefore, specifying `cookieDomain` is less restrictive than omitting it. -For example, if `cookieDomain: nette.org` is set, cookie is also available on all subdomains like `doc.nette.org`. This can also be achieved with the special value `domain`, ie `cookieDomain: domain`. +For example, if `cookieDomain: nette.org` is set, cookies are also available on all subdomains like `doc.nette.org`. This can also be achieved with the special value `domain`, i.e., `cookieDomain: domain`. -The default value of `cookieSecure` is `auto` which means that if the website is running on HTTPS, cookies will be sent with the `Secure` flag and will therefore only be available via HTTPS. +The default value `auto` for the `cookieSecure` attribute means that if the website runs on HTTPS, cookies will be sent with the `Secure` flag and will therefore only be available via HTTPS. HTTP Proxy ---------- -If the site is running behind an HTTP proxy, enter the IP address of the proxy so that detection of HTTPS connections works correctly, as well as the client IP address. That is, so that [Nette\Http\Request::getRemoteAddress()|request#getRemoteAddress] and [isSecured()|request#isSecured] return the correct values and links are generated with the `https:` protocol in the templates. +If the site runs behind an HTTP proxy, enter the proxy's IP address so that HTTPS connection detection and the client's IP address work correctly. That is, so that [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] and [isSecured() |request#isSecured] return the correct values, and links are generated with the `https:` protocol in templates. ```neon http: - # IP address, range (ie. 127.0.0.1/8) or array of these values - proxy: 127.0.0.1 # (string|string[]) defaults to none + # IP address, range (e.g., 127.0.0.1/8), or an array of these values + proxy: 127.0.0.1 # (string|string[]) defaults to not set ``` @@ -115,22 +115,22 @@ Basic [sessions] settings: ```neon session: - # shows session panel in Tracy Bar? + # show the session panel in Tracy Bar? debugger: ... # (bool) defaults to false # inactivity time after which the session expires expiration: 14 days # (string) defaults to '3 hours' - # when to start the session? + # when should the session start? autoStart: ... # (smart|always|never) defaults to 'smart' - # handler, service that implements the SessionHandlerInterface interface + # handler, a service implementing SessionHandlerInterface handler: @handlerService ``` -The `autoStart` option controls when to start the session. The value `always` means that the session is always started when the application starts. The `smart` value means that the session will be started when the application starts only if it already exists, or at the moment we want to read from or write to it. Finally, the value `never` disables the automatic start of the session. This has been in use since "nette/http 3.1.5":[https://github.com/nette/http/releases/tag/v3.1.5]. +The `autoStart` option controls when the session should start. The value `always` means the session starts whenever the application starts. The value `smart` means the session starts with the application only if it already exists, or at the moment we want to read from or write to it. Finally, the value `never` disables the automatic start of the session. -You can also set all PHP [session directives |https://www.php.net/manual/en/session.configuration.php] (in camelCase format) and also [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Example: +Furthermore, you can set all PHP [session directives |https://www.php.net/manual/en/session.configuration.php] (in camelCase format) and also [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Example: ```neon session: @@ -145,15 +145,27 @@ session: Session Cookie -------------- -The session cookie is sent with the same parameters as [other cookie|#HTTP cookie], but you can change these for it: +The session cookie is sent with the same parameters as [other cookies |#HTTP Cookie], but you can change these specifically for it: ```neon session: - # which hosts are allowed to receive the cookie + # domains that can receive the cookie cookieDomain: 'example.com' # (string|domain) - # restrictions when accessing cross-origin request + # restriction for cross-origin access cookieSamesite: None # (Strict|Lax|None) defaults to Lax ``` -The `cookieSamesite` option affects whether the cookie is sent with [cross-origin requests |nette:glossary#SameSite cookie], which provides some protection against [Cross-Site Request Forgery|nette:glossary#cross-site-request-forgery-csrf] attecks. +The `cookieSamesite` attribute affects whether the cookie is sent with [cross-origin requests |nette:glossary#SameSite cookie], which provides some protection against [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF) attacks. + + +DI Services +=========== + +These services are added to the DI container: + +| Name | Type | Description +|-----------------|----------------------------|--------------------------- +| `http.request` | [api:Nette\Http\Request] | [HTTP request| request] +| `http.response` | [api:Nette\Http\Response] | [HTTP response| response] +| `session.session`| [api:Nette\Http\Session] | [session management| sessions] diff --git a/http/en/request.texy b/http/en/request.texy index c6a7380114..ae45438508 100644 --- a/http/en/request.texy +++ b/http/en/request.texy @@ -2,11 +2,11 @@ HTTP Request ************ .[perex] -Nette encapsulates the HTTP request into objects with an understandable API while providing a sanitization filter. +Nette encapsulates the HTTP request into objects with a clear API while providing a sanitization filter. -An HTTP request is an [api:Nette\Http\Request] object, which you get by passing it using [dependency injection |dependency-injection:passing-dependencies]. In presenters simply call `$httpRequest = $this->getHttpRequest()`. +The HTTP request is represented by the [api:Nette\Http\Request] object. If you are working with Nette, this object is automatically created by the framework, and you can have it passed to you using [dependency injection |dependency-injection:passing-dependencies]. In presenters, simply call the `$this->getHttpRequest()` method. If you are working outside the Nette Framework, you can create the object using [#RequestFactory]. -What is important is that Nette when [creating|#RequestFactory] this object, it clears all GET, POST and COOKIE input parameters as well as URLs of control characters and invalid UTF-8 sequences. So you can safely continue working with the data. The cleaned data is then used in presenters and forms. +A major advantage of Nette is that when creating the object, it automatically sanitizes all input parameters (GET, POST, COOKIE) as well as the URL, removing control characters and invalid UTF-8 sequences. You can then safely work with this data. The sanitized data is subsequently used in presenters and forms. → [Installation and requirements |@home#Installation] @@ -14,7 +14,7 @@ What is important is that Nette when [creating|#RequestFactory] this object, it Nette\Http\Request ================== -This object is immutable. It has no setters, it has only one so-called wither `withUrl()`, which does not change the object, but returns a new instance with a modified value. +This object is immutable. It has no setters; it has only one so-called wither, `withUrl()`, which does not change the object but returns a new instance with the modified value. withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] @@ -24,7 +24,7 @@ Returns a clone with a different URL. getUrl(): Nette\Http\UrlScript .[method] ---------------------------------------- -Returns the URL of the request as object [UrlScript|urls#UrlScript]. +Returns the URL of the request as a [UrlScript |urls#UrlScript] object. ```php $url = $httpRequest->getUrl(); @@ -32,12 +32,12 @@ echo $url; // https://nette.org/en/documentation?action=edit echo $url->getHost(); // nette.org ``` -Browsers do not send a fragment to the server, so `$url->getFragment()` will return an empty string. +Warning: Browsers do not send the fragment to the server, so `$url->getFragment()` will return an empty string. -getQuery(string $key=null): string|array|null .[method] -------------------------------------------------------- -Returns GET request parameters: +getQuery(?string $key=null): string|array|null .[method] +-------------------------------------------------------- +Returns GET request parameters. ```php $all = $httpRequest->getQuery(); // array of all URL parameters @@ -45,9 +45,9 @@ $id = $httpRequest->getQuery('id'); // returns GET parameter 'id' (or null) ``` -getPost(string $key=null): string|array|null .[method] ------------------------------------------------------- -Returns POST request parameters: +getPost(?string $key=null): string|array|null .[method] +------------------------------------------------------- +Returns POST request parameters. ```php $all = $httpRequest->getPost(); // array of all POST parameters @@ -57,29 +57,29 @@ $id = $httpRequest->getPost('id'); // returns POST parameter 'id' (or null) getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] ------------------------------------------------------------------------- -Returns [upload|#Uploaded Files] as object [api:Nette\Http\FileUpload]: +Returns an [upload |#Uploaded Files] as a [api:Nette\Http\FileUpload] object: ```php $file = $httpRequest->getFile('avatar'); -if ($file->hasFile()) { // was any file uploaded? - $file->getUntrustedName(); // name of the file sent by user - $file->getSanitizedName(); // the name without dangerous characters +if ($file?->hasFile()) { // was any file uploaded? + $file->getUntrustedName(); // filename sent by the user + $file->getSanitizedName(); // name without dangerous characters } ``` -Specify an array of keys to access the subtree structure. +To access a nested structure, provide an array of keys. ```php //<input type="file" name="my-form[details][avatar]" multiple> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -Because you can't trust data from the outside and therefore don't rely on the form of the structure, this method is safer than `$request->getFiles()['my-form']['details']['avatar']`, which can fail. +Since you cannot trust external data and therefore rely on the structure of the files, this approach is safer than, for example, `$request->getFiles()['my-form']['details']['avatar']`, which might fail. getFiles(): array .[method] --------------------------- -Returns tree of [upload files|#Uploaded Files] in a normalized structure, with each leaf an instance of [api:Nette\Http\FileUpload]: +Returns a tree of [all uploads |#Uploaded Files] in a normalized structure, where the leaves are [api:Nette\Http\FileUpload] objects: ```php $files = $httpRequest->getFiles(); @@ -88,7 +88,7 @@ $files = $httpRequest->getFiles(); getCookie(string $key): string|array|null .[method] --------------------------------------------------- -Returns a cookie or `null` if it does not exist. +Returns a cookie or `null` if it doesn't exist. ```php $sessId = $httpRequest->getCookie('sess_id'); @@ -97,7 +97,7 @@ $sessId = $httpRequest->getCookie('sess_id'); getCookies(): array .[method] ----------------------------- -Returns all cookies: +Returns all cookies. ```php $cookies = $httpRequest->getCookies(); @@ -106,16 +106,16 @@ $cookies = $httpRequest->getCookies(); getMethod(): string .[method] ----------------------------- -Returns the HTTP method with which the request was made. +Returns the HTTP method used for the request. ```php -echo $httpRequest->getMethod(); // GET, POST, HEAD, PUT +$httpRequest->getMethod(); // GET, POST, HEAD, PUT ``` isMethod(string $method): bool .[method] ---------------------------------------- -Checks the HTTP method with which the request was made. The parameter is case-insensitive. +Tests the HTTP method used for the request. The parameter is case-insensitive. ```php if ($httpRequest->isMethod('GET')) // ... @@ -124,7 +124,7 @@ if ($httpRequest->isMethod('GET')) // ... getHeader(string $header): ?string .[method] -------------------------------------------- -Returns an HTTP header or `null` if it does not exist. The parameter is case-insensitive: +Returns an HTTP header or `null` if it doesn't exist. The parameter is case-insensitive. ```php $userAgent = $httpRequest->getHeader('User-Agent'); @@ -133,7 +133,7 @@ $userAgent = $httpRequest->getHeader('User-Agent'); getHeaders(): array .[method] ----------------------------- -Returns all HTTP headers as associative array: +Returns all HTTP headers as an associative array. ```php $headers = $httpRequest->getHeaders(); @@ -141,19 +141,14 @@ echo $headers['Content-Type']; ``` -getReferer(): ?Nette\Http\UrlImmutable .[method] ------------------------------------------------- -What URL did the user come from? Beware, it is not reliable at all. - - isSecured(): bool .[method] --------------------------- -Is the connection encrypted (HTTPS)? You may need to [set up a proxy|configuration#HTTP proxy] for proper functionality. +Is the connection encrypted (HTTPS)? Proper functionality might require [setting up a proxy |configuration#HTTP Proxy]. isSameSite(): bool .[method] ---------------------------- -Is the request coming from the same (sub) domain and is initiated by clicking on a link? Nette uses the `_nss` cookie (formerly `nette-samesite`) to detect it. +Is the request coming from the same (sub)domain and initiated by clicking a link? Nette uses the `_nss` cookie (formerly `nette-samesite`) for detection. isAjax(): bool .[method] @@ -163,17 +158,17 @@ Is it an AJAX request? getRemoteAddress(): ?string .[method] ------------------------------------- -Returns the user's IP address. You may need to [set up a proxy|configuration#HTTP proxy] for proper functionality. +Returns the user's IP address. Proper functionality might require [setting up a proxy |configuration#HTTP Proxy]. getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Returns DNS translation of the user's IP address. You may need to [set up a proxy|configuration#HTTP proxy] for proper functionality. +Returns the DNS translation of the user's IP address. Proper functionality might require [setting up a proxy |configuration#HTTP Proxy]. -getBasicCredentials(): ?string .[method]{data-version:v3.2} ------------------------------------------------------------ -Returns [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication] credentials. +getBasicCredentials(): ?array .[method] +--------------------------------------- +Returns authentication credentials for [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. ```php [$user, $password] = $httpRequest->getBasicCredentials(); @@ -182,7 +177,7 @@ Returns [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web getRawBody(): ?string .[method] ------------------------------- -Returns the body of the HTTP request: +Returns the body of the HTTP request. ```php $body = $httpRequest->getRawBody(); @@ -191,12 +186,12 @@ $body = $httpRequest->getRawBody(); detectLanguage(array $langs): ?string .[method] ----------------------------------------------- -Detects language. As a parameter `$lang`, we pass an array of languages ​​that the application supports, and it returns the one preferred by browser. It is not magic, the method just uses the `Accept-Language` header. If no match is reached, it returns `null`. +Detects the language. Pass an array of languages supported by the application as the `$langs` parameter, and it will return the one preferred by the visitor's browser. It's not magic; it just uses the `Accept-Language` header. If no match is found, it returns `null`. ```php -// Header sent by browser: Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 +// Browser sends e.g., Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 -$langs = ['hu', 'pl', 'en']; // languages supported in application +$langs = ['hu', 'pl', 'en']; // languages supported by the application echo $httpRequest->detectLanguage($langs); // en ``` @@ -204,41 +199,48 @@ echo $httpRequest->detectLanguage($langs); // en RequestFactory ============== -The object of the current HTTP request is created by [api:Nette\Http\RequestFactory]. If you are writing an application that does not use a DI container, you create a request as follows: +The [api:Nette\Http\RequestFactory] class is used to create an instance of `Nette\Http\Request`, which represents the current HTTP request. (If you are working with Nette, the HTTP request object is automatically created by the framework.) ```php $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -RequestFactory can be configured before calling `fromGlobals()`. We can disable all sanitization of input parameters from invalid UTF-8 sequences using `$factory->setBinary()`. And also set up a proxy server, which is important for the correct detection of the user's IP address using `$factory->setProxy(...)`. +The `fromGlobals()` method creates the request object based on the current PHP global variables (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES`, and `$_SERVER`). When creating the object, it automatically cleanses all input parameters (GET, POST, COOKIE) as well as the URL from control characters and invalid UTF-8 sequences, ensuring security when working with this data later. -It's possible to clean up URLs from characters that can get into them because of poorly implemented comment systems on various other websites by using filters: +RequestFactory can be configured before calling `fromGlobals()`: + +- using the `$factory->setBinary()` method disables automatic cleansing of input parameters from control characters and invalid UTF-8 sequences. +- using the `$factory->setProxy(...)` method specifies the IP address of the [proxy server |configuration#HTTP Proxy], which is necessary for correct detection of the user's IP address. + +RequestFactory allows defining filters that automatically transform parts of the URL request. These filters remove unwanted characters from URLs that might have been inserted, for example, by incorrect implementations of comment systems on various websites: ```php -// remove spaces from path +// remove spaces from the path $requestFactory->urlFilters['path']['%20'] = ''; -// remove dot, comma or right parenthesis form the end of the URL +// remove dot, comma, or right parenthesis from the end of the URI $requestFactory->urlFilters['url']['[.,)]$'] = ''; -// clean the path from duplicated slashes (default filter) +// clean the path from double slashes (default filter) $requestFactory->urlFilters['path']['/{2,}'] = '/'; ``` +The first key, `'path'` or `'url'`, determines which part of the URL the filter will be applied to. The second key is the regular expression to search for, and the value is the replacement to be used instead of the found text. + Uploaded Files ============== -Method `Nette\Http\Request::getFiles()` return a tree of upload files in a normalized structure, with each leaf an instance of [api:Nette\Http\FileUpload]. These objects encapsulate the data submitted by the `<input type=file>` form element. +The `Nette\Http\Request::getFiles()` method returns an array of all uploads in a normalized structure, where the leaves are [api:Nette\Http\FileUpload] objects. These encapsulate the data submitted by the `<input type=file>` form element. -The structure reflects the naming of elements in HTML. In the simplest example, this might be a single named form element submitted as: +The structure reflects the naming of elements in HTML. In the simplest case, it might be a single named form element submitted as: ```latte <input type="file" name="avatar"> ``` -In this case, the `$request->getFiles()` returns array: +In this case, `$request->getFiles()` returns an array: ```php [ @@ -246,19 +248,19 @@ In this case, the `$request->getFiles()` returns array: ] ``` -The `FileUpload` object is created even if the user did not upload any file or the upload failed. Method `hasFile()` returns true if a file has been sent: +The `FileUpload` object is created even if the user did not upload any file or the upload failed. The `hasFile()` method returns true if a file was sent: ```php -$request->getFile('avatar')->hasFile(); +$request->getFile('avatar')?->hasFile(); ``` -In the case of an input using array notation for the name: +In the case of an element name using array notation: ```latte <input type="file" name="my-form[details][avatar]"> ``` -returned tree ends up looking like this: +the returned tree looks like this: ```php [ @@ -273,10 +275,10 @@ returned tree ends up looking like this: You can also create arrays of files: ```latte -<input type="file" name="my-form[details][avatars][] multiple"> +<input type="file" name="my-form[details][avatars][]" multiple> ``` -In such a case structure looks like: +In such a case, the structure looks like this: ```php [ @@ -292,16 +294,16 @@ In such a case structure looks like: ] ``` -The best way to access index 1 of a nested array is as follows: +The best way to access index 1 of the nested array is as follows: ```php $file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { +if ($file instanceof Nette\Http\FileUpload) { // ... } ``` -Because you can't trust data from the outside and therefore don't rely on the form of the structure, this method is safer than `$request->getFiles()['my-form']['details']['avatars'][1]`, which can fail. +Since you cannot trust external data and therefore rely on the structure of the files, this approach is safer than, for example, `$request->getFiles()['my-form']['details']['avatars'][1]`, which might fail. Overview of `FileUpload` Methods .{toc: FileUpload} @@ -310,7 +312,7 @@ Overview of `FileUpload` Methods .{toc: FileUpload} hasFile(): bool .[method] ------------------------- -Returns `true` if the user has uploaded a file. +Returns `true` if the user uploaded a file. isOk(): bool .[method] @@ -320,7 +322,7 @@ Returns `true` if the file was uploaded successfully. getError(): int .[method] ------------------------- -Returns the error code associated with the uploaded file. It is be one of [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php] constants. If the file was uploaded successfully, it returns `UPLOAD_ERR_OK`. +Returns the error code associated with the uploaded file. It is one of the [UPLOAD_ERR_XXX |https://php.net/manual/en/features.file-upload.errors.php] constants. If the file was uploaded successfully, it returns `UPLOAD_ERR_OK`. move(string $dest) .[method] @@ -342,12 +344,12 @@ getContentType(): ?string .[method] Detects the MIME content type of the uploaded file based on its signature. If the upload was not successful or the detection failed, it returns `null`. .[caution] -Requires PHP extension `fileinfo`. +Requires the PHP extension `fileinfo`. getUntrustedName(): string .[method] ------------------------------------ -Returns the original file name as submitted by the browser. In nette/http 3.0 and earlier, the method was called `getName()`. +Returns the original file name as submitted by the browser. .[caution] Do not trust the value returned by this method. A client could send a malicious filename with the intention to corrupt or hack your application. @@ -355,12 +357,23 @@ Do not trust the value returned by this method. A client could send a malicious getSanitizedName(): string .[method] ------------------------------------ -Returns the sanitized file name. It contains only ASCII characters `[a-zA-Z0-9.-]`. If the name does not contain such characters, it returns 'unknown'. If the file is JPEG, PNG, GIF, or WebP image, it returns the correct file extension. +Returns the sanitized file name. It contains only ASCII characters `[a-zA-Z0-9.-]`. If the name does not contain such characters, it returns `'unknown'`. If the file is a JPEG, PNG, GIF, WebP, or AVIF image, it also returns the correct file extension. + +.[caution] +Requires the PHP extension `fileinfo`. -getUntrustedFullPath(): string .[method]{data-version:v3.2} ------------------------------------------------------------ -Returns the original full path as submitted by the browser during directory upload. The full path is only available in PHP 8.1 and above. In previous versions, this method returns the untrusted file name. +getSuggestedExtension(): ?string .[method]{data-version:3.2.4} +-------------------------------------------------------------- +Returns the appropriate file extension (without the dot) corresponding to the detected MIME type. + +.[caution] +Requires the PHP extension `fileinfo`. + + +getUntrustedFullPath(): string .[method] +---------------------------------------- +Returns the original file path as submitted by the browser during directory upload. The full path is only available in PHP 8.1 and later. In previous versions, this method returns the original file name. .[caution] Do not trust the value returned by this method. A client could send a malicious filename with the intention to corrupt or hack your application. @@ -373,22 +386,22 @@ Returns the size of the uploaded file. If the upload was not successful, it retu getTemporaryFile(): string .[method] ------------------------------------ -Returns the path of the temporary location of the uploaded file. If the upload was not successful, it returns `''`. +Returns the path to the temporary location of the uploaded file. If the upload was not successful, it returns `''`. isImage(): bool .[method] ------------------------- -Returns `true` if the uploaded file is a JPEG, PNG, GIF, or WebP image. Detection is based on its signature. The integrity of the entire file is not checked. You can find out if an image is not corrupted for example by trying to [load it|#toImage]. +Returns `true` if the uploaded file is a JPEG, PNG, GIF, WebP, or AVIF image. Detection is based on its signature and does not verify the integrity of the entire file. Whether an image is corrupted can be determined, for example, by trying to [load it |#toImage]. .[caution] -Requires PHP extension `fileinfo`. +Requires the PHP extension `fileinfo`. getImageSize(): ?array .[method] -------------------------------- -Returns a pair of `[width, height]` with dimensions of the uploaded image. If the upload was not successful or is not a valid image, it returns `null`. +Returns a pair `[width, height]` with the dimensions of the uploaded image. If the upload was not successful or it is not a valid image, it returns `null`. toImage(): Nette\Utils\Image .[method] -------------------------------------- -Loads an image as an [Image|utils:images] object. If the upload was not successful or is not a valid image, it throws an `Nette\Utils\ImageException` exception. +Loads the image as an [Image |utils:images] object. If the upload was not successful or it is not a valid image, it throws an `Nette\Utils\ImageException`. diff --git a/http/en/response.texy b/http/en/response.texy index 4e7d5ec26f..7654f039e9 100644 --- a/http/en/response.texy +++ b/http/en/response.texy @@ -2,9 +2,9 @@ HTTP Response ************* .[perex] -Nette encapsulates the HTTP response into objects with an understandable API while providing a sanitization filter. +Nette encapsulates the HTTP response into objects with a clear API. -An HTTP response is an [api:Nette\Http\Response] object, which you get by passing it using [dependency injection |dependency-injection:passing-dependencies]. In presenters simply call `$httpResponse = $this->getHttpResponse()`. +The HTTP response is represented by the [api:Nette\Http\Response] object. If you are working with Nette, this object is automatically created by the framework, and you can have it passed to you using [dependency injection |dependency-injection:passing-dependencies]. In presenters, simply call the `$this->getHttpResponse()` method. → [Installation and requirements |@home#Installation] @@ -12,15 +12,15 @@ An HTTP response is an [api:Nette\Http\Response] object, which you get by passin Nette\Http\Response =================== -Unlike [Nette\Http\Request|request], this object is mutable, so you can use setters to change the state, ie to send headers. Remember that all setters **must be called before any actual output is sent.** The `isSent()` method tells if output have been sent. If it returns `true`, each attempt to send a header throws an `Nette\InvalidStateException` exception. +Unlike [Nette\Http\Request |request], this object is mutable, so you can use setters to change the state, e.g., to send headers. Remember that all setters **must be called before any actual output is sent.** The `isSent()` method indicates if the output has already been sent. If it returns `true`, any attempt to send a header will throw an `Nette\InvalidStateException`. -setCode(int $code, string $reason=null) .[method] -------------------------------------------------- -Changes a status [response code |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. For better source code readability it is recommended to use [predefined constants |api:Nette\Http\IResponse] instead of actual numbers. +setCode(int $code, ?string $reason=null) .[method] +-------------------------------------------------- +Changes the status [response code |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. For better source code readability, it is recommended to use [predefined constants |api:Nette\Http\IResponse] instead of actual numbers. ```php -$httpResponse->setCode(Nette\Http\Response::S404_NOT_FOUND); +$httpResponse->setCode(Nette\Http\Response::S404_NotFound); ``` @@ -31,12 +31,12 @@ Returns the status code of the response. isSent(): bool .[method] ------------------------ -Returns whether headers have already been sent from the server to the browser, so it is no longer possible to send headers or change the status code. +Returns whether headers have already been sent from the server to the browser, meaning it is no longer possible to send headers or change the status code. setHeader(string $name, string $value) .[method] ------------------------------------------------ -Sends an HTTP header and **overwrites** previously sent header of the same name. +Sends an HTTP header and **overwrites** a previously sent header of the same name. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -45,7 +45,7 @@ $httpResponse->setHeader('Pragma', 'no-cache'); addHeader(string $name, string $value) .[method] ------------------------------------------------ -Sends an HTTP header and **doesn't overwrite** previously sent header of the same name. +Sends an HTTP header and **does not overwrite** a previously sent header of the same name. ```php $httpResponse->addHeader('Accept', 'application/json'); @@ -60,7 +60,7 @@ Deletes a previously sent HTTP header. getHeader(string $header): ?string .[method] -------------------------------------------- -Returns the sent HTTP header, or `null` if it does not exist. The parameter is case-insensitive. +Returns the sent HTTP header or `null` if it doesn't exist. The parameter is case-insensitive. ```php $pragma = $httpResponse->getHeader('Pragma'); @@ -69,7 +69,7 @@ $pragma = $httpResponse->getHeader('Pragma'); getHeaders(): array .[method] ----------------------------- -Returns all sent HTTP headers as associative array. +Returns all sent HTTP headers as an associative array. ```php $headers = $httpResponse->getHeaders(); @@ -77,18 +77,18 @@ echo $headers['Pragma']; ``` -setContentType(string $type, string $charset=null) .[method] ------------------------------------------------------------- -Sends the header `Content-Type`. +setContentType(string $type, ?string $charset=null) .[method] +------------------------------------------------------------- +Changes the `Content-Type` header. ```php $httpResponse->setContentType('text/plain', 'UTF-8'); ``` -redirect(string $url, int $code=self::S302_FOUND): void .[method] +redirect(string $url, int $code=self::S302_Found): void .[method] ----------------------------------------------------------------- -Redirects to another URL. Don't forget to quit the script then. +Redirects to another URL. Remember to terminate the script afterwards. ```php $httpResponse->redirect('http://example.com'); @@ -108,42 +108,42 @@ $httpResponse->setExpiration('1 hour'); sendAsFile(string $fileName) .[method] -------------------------------------- -Response should be downloaded with *Save as* dialog with specified name. It does not send any file itself to output. +The response will be downloaded via a *Save as* dialog box with the specified name. It does not send the file itself. ```php $httpResponse->sendAsFile('invoice.pdf'); ``` -setCookie(string $name, string $value, $time, string $path=null, string $domain=null, bool $secure=null, bool $httpOnly=null, string $sameSite=null) .[method] --------------------------------------------------------------------------------------------------------------------------------------------------------------- +setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------- Sends a cookie. Default parameter values: -| `$path` | `'/'` | with scope to all paths on (sub)domain *(configurable)* -| `$domain` | `null` | with scope of the current (sub)domain, but not its subdomains *(configurable)* +| `$path` | `'/'` | cookie is available for all paths within the (sub)domain *(configurable)* +| `$domain` | `null` | meaning available for the current (sub)domain, but not its subdomains *(configurable)* | `$secure` | `true` | if the site is running on HTTPS, otherwise `false` *(configurable)* | `$httpOnly` | `true` | cookie is inaccessible to JavaScript -| `$sameSite` | `'Lax'` | cookie does not have to be sent when [accessed from another origin|nette:glossary#SameSite cookie] +| `$sameSite` | `'Lax'` | cookie might not be sent during [cross-origin access |nette:glossary#SameSite cookie] -You can change the default values of the `$path`, `$domain` and `$secure` parameters in [configuration#HTTP cookie]. +You can change the default values of the `$path`, `$domain`, and `$secure` parameters in the [configuration |configuration#HTTP Cookie]. -The time can be specified as number of seconds or a string: +The time can be specified as a number of seconds or a string: ```php $httpResponse->setCookie('lang', 'en', '100 days'); ``` -The `$domain` option determines which domains (origins) can accept cookies. If not specified, the cookie is accepted by the same (sub)domain as is set by it, excluding their subdomains. If `$domain` is specified, then subdomains are also included. Therefore, specifying `$domain` is less restrictive than omitting. For example, if `$domain = 'nette.org'`, cookie is also available on all subdomains like `doc.nette.org`. +The `$domain` parameter determines which domains can accept the cookie. If not specified, the cookie is accepted by the same (sub)domain that set it, but not its subdomains. If `$domain` is specified, subdomains are also included. Therefore, specifying `$domain` is less restrictive than omitting it. For example, with `$domain = 'nette.org'`, cookies are also available on all subdomains like `doc.nette.org`. -You can use the `Response::SAME_SITE_LAX`, `SAME_SITE_STRICT` and `SAME_SITE_NONE` constants for the `$sameSite` value. +You can use the constants `Response::SameSiteLax`, `Response::SameSiteStrict`, and `Response::SameSiteNone` for the `$sameSite` value. -deleteCookie(string $name, string $path=null, string $domain=null, bool $secure=null): void .[method] ------------------------------------------------------------------------------------------------------ -Deletes a cookie. The default values ​​of the parameters are: +deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] +-------------------------------------------------------------------------------------------------------- +Deletes a cookie. The default values of the parameters are: - `$path` with scope to all directories (`'/'`) -- `$domain` with scope of the current (sub)domain, but not its subdomains -- `$secure` is affected by the settings in [configuration#HTTP cookie] +- `$domain` with scope to the current (sub)domain, but not its subdomains +- `$secure` depends on the settings in the [configuration |configuration#HTTP Cookie] ```php $httpResponse->deleteCookie('lang'); diff --git a/http/en/sessions.texy b/http/en/sessions.texy index 63e15f6fa3..f0831458d5 100644 --- a/http/en/sessions.texy +++ b/http/en/sessions.texy @@ -3,19 +3,19 @@ Sessions <div class=perex> -HTTP is a stateless protocol, but almost every application needs to keep state between requests, e.g. content of a shopping cart. This is what a session is used for. Let's see +HTTP is a stateless protocol; however, almost every application needs to maintain state between requests, such as the content of a shopping cart. This is precisely what sessions are used for. We will show: - how to use sessions -- how to avoid naming conflicts +- how to prevent naming conflicts - how to set expiration </div> -When using sessions, each user receives a unique identifier called session ID, which is passed in a cookie. This serves as the key to the session data. Unlike cookies, which are stored on the browser side, session data is stored on the server side. +When using sessions, each user receives a unique identifier called a session ID, which is passed in a cookie. This serves as a key to the session data. Unlike cookies, which are stored on the browser side, session data is stored on the server side. -We configure session in [configuration |configuration#session], the choice of expiration time is important. +We configure sessions in the [configuration |configuration#Session]; the choice of expiration time is particularly important. -The session is managed by the [api:Nette\Http\Session] object, which you get by passing it using [dependency injection |dependency-injection:passing-dependencies]. In presenters simply call `$session = $this->getSession()`. +Session management is handled by the [api:Nette\Http\Session] object, which you can access by having it passed via [dependency injection |dependency-injection:passing-dependencies]. In presenters, simply call `$session = $this->getSession()`. → [Installation and requirements |@home#Installation] @@ -23,59 +23,49 @@ The session is managed by the [api:Nette\Http\Session] object, which you get by Starting Session ================ -By default, Nette will start a session automatically the moment we start reading from it or writing data to it. To manually start a session, use `$session->start()`. +By default, Nette automatically starts a session the moment we begin reading from or writing data to it. To start a session manually, use `$session->start()`. -PHP sends HTTP headers affecting caching when starting the session, see [php:session_cache_limiter], and possibly a cookie with the session ID. Therefore, it is always necessary to start the session before sending any output to the browser, otherwise an exception will be thrown. So if you know that a session will be used during page rendering, start it manually before, for example in the presenter. +PHP sends HTTP headers affecting caching when starting the session (see [php:session_cache_limiter]), and possibly a cookie with the session ID. Therefore, it is always necessary to start the session before sending any output to the browser; otherwise, an exception will be thrown. So, if you know that a session will be used during page rendering, start it manually beforehand, for example, in the presenter. -In developer mode, Tracy starts the session because it uses it to display redirection and AJAX requests bars in the Tracy Bar. +In developer mode, Tracy starts the session because it uses it to display bars for redirects and AJAX requests in the Tracy Bar. -Section -======= +Sections +======== -In pure PHP, the session data store is implemented as an array accessible via a global variable `$_SESSION`. The problem is that applications normally consist of a number of independent parts, and if all have only one same array available, sooner or later a name collision will occur. +In pure PHP, the session data storage is implemented as an array accessible via the global variable `$_SESSION`. The problem is that applications typically consist of many independent parts, and if all of them have only one array available, sooner or later a name collision will occur. -Nette Framework solves the problem by dividing the entire space into sections (objects [api:Nette\Http\SessionSection]). Each unit then uses its own section with a unique name and no collisions can occur. +Nette Framework solves this problem by dividing the entire space into sections (objects of [api:Nette\Http\SessionSection]). Each unit then uses its own section with a unique name, and no collision can occur. -We get the section from the session manager: +We obtain a section from the session: ```php $section = $session->getSection('unique name'); ``` -In the presenter it is enough to call `getSession()` with the parameter: +In the presenter, just use `getSession()` with a parameter: ```php -// $this is Presenter +// $this is a Presenter $section = $this->getSession('unique name'); ``` -The existence of the section can be checked by the method `$session->hasSection('unique name')`. +The existence of a section can be checked using the `$session->hasSection('unique name')` method. -The section itself is very easy to work with using the `set()`, `get()` and `remove()` methods: (since "nette/http v3.1.5":[https://github.com/nette/http/releases/tag/v3.1.5]) +Working with the section itself is then very easy using the `set()`, `get()`, and `remove()` methods: ```php -// variable writing -$section->set('userName', 'franta'); +// writing a variable +$section->set('userName', 'john'); -// reading a variable, returns null if it does not exist +// reading a variable, returns null if it doesn't exist echo $section->get('userName'); -// variable removing +// removing a variable $section->remove('userName'); ``` -Before version 3.1.5, magic variables or ArrayAccess were used, which will be deprecated in version 4. - -```php -$section->userName = 'john'; // or $section['userName'] = 'john'; - -echo $section->userName; // or echo $section['userName']; - -unset($section->userName); // unset($section['userName']); -``` - -It's possible to use `foreach` cycle to obtain all variables from section: +To get all variables from a section, you can use a `foreach` loop: ```php foreach ($section as $key => $val) { @@ -87,36 +77,30 @@ foreach ($section as $key => $val) { How to Set Expiration --------------------- -Expiration can be set for individual sections or even individual variables. We can let the user's login expire in 20 minutes, but still remember the contents of a shopping cart. +Expiration can be set for individual sections or even individual variables. We can let a user's login expire after 20 minutes, while still remembering the contents of the shopping cart. ```php -// section will expire after 20 minutes +// section expires after 20 minutes $section->setExpiration('20 minutes'); ``` -The third parameter of the `set()` method is used to set the expiration of individual variables: (since "nette/http v3.1.5":[https://github.com/nette/http/releases/tag/v3.1.5]) +To set the expiration for individual variables, use the third parameter of the `set()` method: ```php // variable 'flash' expires after 30 seconds $section->set('flash', $message, '30 seconds'); ``` -Before version 3.1.5 the `setExpiration()` method was used: - -```php -$section->setExpiration('30 seconds', 'flash'); -``` - .[note] -Remember that the expiration time of the whole session (see [session configuration |configuration#session]) must be equal to or higher than the time set for individual sections or variables. +Remember that the expiration time of the entire session (see [session configuration |configuration#Session]) must be equal to or greater than the time set for individual sections or variables. -The cancellation of the previously set expiration can be achieved by the method `removeExpiration()`. Immediate deletion of the whole section will be ensured by the method `remove()`. +To cancel a previously set expiration, use the `removeExpiration()` method. To immediately remove the entire section, use the `remove()` method. -Events $onStart, $onBeforeWrite .{data-version:v3.1.5} ------------------------------------------------------- +Events $onStart, $onBeforeWrite +------------------------------- -Object `Nette\Http\Session` has [events|nette:glossary#Events] `$onStart` a `$onBeforeWrite`, so you can add callbacks that are called after the session starts or before it is written to disk and then terminated. +The `Nette\Http\Session` object has [events |nette:glossary#Events] `$onStart` and `$onBeforeWrite`, so you can add callbacks that are invoked after the session starts or before it is written to disk and subsequently terminated. ```php $session->onBeforeWrite[] = function () { @@ -136,7 +120,7 @@ Overview of methods of the `Nette\Http\Session` class for session management: start(): void .[method] ----------------------- -Starts a session. +Starts the session. isStarted(): bool .[method] @@ -146,7 +130,7 @@ Is the session started? close(): void .[method] ----------------------- -Ends the session. The session ends automatically at the end of the script. +Ends the session. The session automatically ends at the end of the script execution. destroy(): void .[method] @@ -161,7 +145,7 @@ Does the HTTP request contain a cookie with a session ID? regenerateId(): void .[method] ------------------------------ -Generates a new random session ID. Data remain unchanged. +Generates a new random session ID. Data remains preserved. getId(): string .[method] @@ -174,34 +158,34 @@ Returns the session ID. Configuration ------------- -We configure session in [configuration |configuration#session]. If you are writing an application that does not use a DI container, use these methods to configure it. They must be called before starting the session. +We configure the session in the [configuration |configuration#Session]. If you are writing an application that does not use a DI container, use these methods for configuration. They must be called before starting the session. <div class=wiki-methods-brief> setName(string $name): static .[method] --------------------------------------- -Sets the name of the cookie which is used to transmit the session ID. The default name is `PHPSESSID`. This is useful if you run several different applications on the same site. +Sets the name of the cookie in which the session ID is transmitted. The standard name is `PHPSESSID`. This is useful if you run several different applications on the same website. getName(): string .[method] --------------------------- -Returns the name of session cookie. +Returns the name of the cookie in which the session ID is transmitted. setOptions(array $options): static .[method] -------------------------------------------- -Configures the session. It is possible to set all PHP [session directives |https://www.php.net/manual/en/session.configuration.php] (in camelCase format, eg write `savePath` instead of `session.save_path`) and also [readAndClose|https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. +Configures the session. It is possible to set all PHP [session directives |https://www.php.net/manual/en/session.configuration.php] (in camelCase format, e.g., write `savePath` instead of `session.save_path`) and also [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. setExpiration(?string $time): static .[method] ---------------------------------------------- -Sets the time of inactivity after which the session expires. +Sets the inactivity time after which the session expires. -setCookieParameters(string $path, string $domain=null, bool $secure=null, string $samesite=null): static .[method] ------------------------------------------------------------------------------------------------------------------- -Sets parameters for cookies. You can change the default parameter values in [configuration#Session cookie]. +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] +--------------------------------------------------------------------------------------------------------------------- +Sets parameters for cookies. You can change the default parameter values in the [configuration |configuration#Session Cookie]. setSavePath(string $path): static .[method] @@ -211,7 +195,7 @@ Sets the directory where session files are stored. setHandler(\SessionHandlerInterface $handler): static .[method] --------------------------------------------------------------- -Sets custom handler, see [PHP documentation |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. +Sets a custom handler, see the [PHP documentation |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. </div> @@ -219,8 +203,9 @@ Sets custom handler, see [PHP documentation |https://www.php.net/manual/en/class Safety First ============ -The server assumes that it communicates with the same user as long as requests contain the same session ID. The task of security mechanisms is to ensure that this behavior really works and that there is no possibility to substitute or steal an identifier. +The server assumes that it communicates with the same user as long as requests are accompanied by the same session ID. The task of security mechanisms is to ensure that this is indeed the case and that the identifier cannot be stolen or substituted. -That's why Nette Framework properly configures PHP directives to transfer session ID only in cookies, to avoid access from JavaScript and to ignore the identifiers in the URL. Moreover in critical moments, such as user login, it generates a new Session ID. +Nette Framework therefore correctly configures PHP directives to transfer the session ID only in cookies, make it inaccessible to JavaScript, and ignore any identifiers in the URL. Moreover, at critical moments, such as user login, it generates a new session ID. -Function ini_set is used for configuring PHP, but unfortunately, its use is prohibited at some web hosting services. If it's your case, try to ask your hosting provider to allow this function for you, or at least to configure his server properly. .[note] +.[note] +The `ini_set` function is used for configuring PHP, but unfortunately, some hosting providers prohibit its use. If this is the case with your host, try to arrange with them to allow this function for you or at least configure the server properly. diff --git a/http/en/urls.texy b/http/en/urls.texy index 3118a1f17a..84a947c12f 100644 --- a/http/en/urls.texy +++ b/http/en/urls.texy @@ -1,8 +1,8 @@ -URL Parser and Builder -********************** +Working with URLs +***************** .[perex] -The [#Url], [#UrlImmutable], and [#UrlScript] classes make it easy to manage, parse, and manipulate URLs. +The [#Url], [#UrlImmutable], and [#UrlScript] classes make it easy to generate, parse, and manipulate URLs. → [Installation and requirements |@home#Installation] @@ -10,7 +10,7 @@ The [#Url], [#UrlImmutable], and [#UrlScript] classes make it easy to manage, pa Url === -The [api:Nette\Http\Url] class makes it easy to work with the URL and its individual components, which are outlined in this diagram: +The [api:Nette\Http\Url] class allows easy manipulation of URLs and their individual components, as depicted in this diagram: /--pre scheme user password host port path query fragment @@ -22,7 +22,7 @@ The [api:Nette\Http\Url] class makes it easy to work with the URL and its indivi hostUrl authority \-- -URL generation is intuitive: +Generating URLs is intuitive: ```php use Nette\Http\Url; @@ -36,15 +36,26 @@ $url->setScheme('https') echo $url; // 'https://localhost/edit?foo=bar' ``` -You can also parse the URL and then manipulate it: +You can also parse a URL and then manipulate it: ```php $url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer' + 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', ); ``` -The following methods are available to get or change individual URL components: +The `Url` class implements the `JsonSerializable` interface and has a `__toString()` method, so the object can be printed or used in data passed to `json_encode()`. + +```php +echo $url; +echo json_encode([$url]); +``` + + +URL Components .[method] +------------------------ + +The following methods are available to retrieve or modify individual URL components: .[language-php] | Setter | Getter | Returned value @@ -58,11 +69,13 @@ The following methods are available to get or change individual URL components: | `setPath(string $path)` | `getPath(): string` | `'/en/download'` | `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` | `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'nette.org:8080'` -| | `getHostUrl(): string` | `'http://nette.org:8080'` -| | `getAbsoluteUrl(): string` | full URL +| | `getAuthority(): string` | `'john:xyz*12@nette.org:8080'` +| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` +| | `getAbsoluteUrl(): string` | entire URL + +Warning: When working with a URL obtained from an [HTTP request |request], keep in mind that it will not contain the fragment, as the browser does not send it to the server. -We can also operate with individual query parameters using: +We can also work with individual query parameters using: .[language-php] | Setter | Getter @@ -70,7 +83,10 @@ We can also operate with individual query parameters using: | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` -Method `getDomain(int $level = 2)` returns the right or left part of the host. This is how it works if the host is `www.nette.org`: + +getDomain(int $level = 2): string .[method] +------------------------------------------- +Returns the right or left part of the host. Here's how it works if the host is `www.nette.org`: .[language-php] | `getDomain(1)` | `'org'` @@ -82,41 +98,68 @@ Method `getDomain(int $level = 2)` returns the right or left part of the host. T | `getDomain(-3)` | `''` -The `Url` class implements the `JsonSerializable` interface and has a `__toString()` method so that the object can be printed or used in data passed to `json_encode()`. +isEqual(string|Url $anotherUrl): bool .[method] +----------------------------------------------- +Checks if two URLs are identical. ```php -echo $url; -echo json_encode([$url]); +$url->isEqual('https://nette.org'); ``` -Method `isEqual(string|Url $anotherUrl): bool` tests whether the two URLs are identical. + +Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} +---------------------------------------------------------------- +Checks if a URL is absolute. A URL is considered absolute if it begins with a scheme (e.g., http, https, ftp) followed by a colon. ```php -$url->isEqual('https://nette.org'); +Url::isAbsolute('https://nette.org'); // true +Url::isAbsolute('//nette.org'); // false +``` + + +Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} +-------------------------------------------------------------------------- +Normalizes a URL path by removing special segments `.` and `..`. This method removes redundant path elements in the same way web browsers do. + +```php +Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' +Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' +Url::removeDotSegments('./today/../file.txt'); // 'file.txt' ``` UrlImmutable ============ -The class [api:Nette\Http\UrlImmutable] is an immutable alternative to class `Url` (just as in PHP `DateTimeImmutable` is immutable alternative to `DateTime`). Instead of setters, it has so-called withers, which do not change the object, but return new instances with a modified value: +The [api:Nette\Http\UrlImmutable] class is an immutable alternative to the [#Url] class (similar to how `DateTimeImmutable` is an immutable alternative to `DateTime` in PHP). Instead of setters, it has "withers," which do not change the object but return new instances with the modified value: ```php use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer' + 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', ); $newUrl = $url ->withUser('') ->withPassword('') - ->withPath('/cs/'); + ->withPath('/en/'); + +echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/en/?name=param#footer' +``` -echo $newUrl; // 'http://nette.org:8080/cs/?name=param#footer' +The `UrlImmutable` class implements the `JsonSerializable` interface and has a `__toString()` method, so the object can be printed or used in data passed to `json_encode()`. + +```php +echo $url; +echo json_encode([$url]); ``` -The following methods are available to get or change individual URL components: + +URL Components .[method] +------------------------ + +The following methods are available to retrieve or change individual URL components: .[language-php] | Wither | Getter | Returned value @@ -130,11 +173,13 @@ The following methods are available to get or change individual URL components: | `withPath(string $path)` | `getPath(): string` | `'/en/download'` | `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` | `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'nette.org:8080'` -| | `getHostUrl(): string` | `'http://nette.org:8080'` -| | `getAbsoluteUrl(): string` | full URL +| | `getAuthority(): string` | `'john:xyz*12@nette.org:8080'` +| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` +| | `getAbsoluteUrl(): string` | entire URL + +The `withoutUserInfo()` method removes `user` and `password`. -We can also operate with individual query parameters using: +We can also work with individual query parameters using: .[language-php] | Wither | Getter @@ -142,22 +187,52 @@ We can also operate with individual query parameters using: | `withQuery(string\|array $query)` | `getQueryParameters(): array` | `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` -The `getDomain(int $level = 2)` method works the same as the method in `Url`. Method `withoutUserInfo()` removes `user` and `password`. -The `UrlImmutable` class implements the `JsonSerializable` interface and has a `__toString()` method so that the object can be printed or used in data passed to `json_encode()`. +getDomain(int $level = 2): string .[method] +------------------------------------------- +Returns the right or left part of the host. Here's how it works if the host is `www.nette.org`: + +.[language-php] +| `getDomain(1)` | `'org'` +| `getDomain(2)` | `'nette.org'` +| `getDomain(3)` | `'www.nette.org'` +| `getDomain(0)` | `'www.nette.org'` +| `getDomain(-1)` | `'www.nette'` +| `getDomain(-2)` | `'www'` +| `getDomain(-3)` | `''` + + +resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} +---------------------------------------------------------------------- +Resolves an absolute URL in the same way a browser processes links on an HTML page: +- if the link is an absolute URL (contains a scheme), it is used unchanged +- if the link begins with `//`, only the scheme from the current URL is adopted +- if the link begins with `/`, an absolute path from the domain root is created +- in other cases, the URL is constructed relative to the current path ```php -echo $url; -echo json_encode([$url]); +$url = new UrlImmutable('https://example.com/path/page'); +echo $url->resolve('../foo'); // 'https://example.com/foo' +echo $url->resolve('/bar'); // 'https://example.com/bar' +echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.html' ``` -Method `isEqual(string|Url $anotherUrl): bool` tests whether the two URLs are identical. + +isEqual(string|Url $anotherUrl): bool .[method] +----------------------------------------------- +Checks if two URLs are identical. + +```php +$url->isEqual('https://nette.org'); +``` UrlScript ========= -The [api:Nette\Http\UrlScript] class is a descendant of `UrlImmutable` and additionally distinguishes these logical parts of the URL: +The [api:Nette\Http\UrlScript] class is a descendant of [#UrlImmutable] and extends it with additional virtual URL components, such as the project's root directory, etc. Like its parent class, it is an immutable object. + +The following diagram shows the components that UrlScript recognizes: /--pre baseUrl basePath relativePath relativeUrl @@ -169,7 +244,14 @@ The [api:Nette\Http\UrlScript] class is a descendant of `UrlImmutable` and addit scriptPath pathInfo \-- -The following methods are available to get these parts: +- `baseUrl` is the base URL of the application, including the domain and the path part to the application's root directory +- `basePath` is the path part to the application's root directory +- `scriptPath` is the path to the current script +- `relativePath` is the script name (and possibly additional path segments) relative to `basePath` +- `relativeUrl` is the entire part of the URL after `baseUrl`, including the query string and fragment +- `pathInfo` is a now rarely used part of the URL after the script name + +The following methods are available to retrieve these parts of the URL: .[language-php] | Getter | Returned value @@ -181,5 +263,4 @@ The following methods are available to get these parts: | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` - -We do not create objects `UrlScript` directly, but the method [Nette\Http\Request::getUrl() |request] returns it. +We usually do not create `UrlScript` objects directly; instead, the [Nette\Http\Request::getUrl() |request] method returns it with the components already correctly set for the current HTTP request. diff --git a/http/meta.json b/http/meta.json index 6422f8f332..e2bea22602 100644 --- a/http/meta.json +++ b/http/meta.json @@ -1,5 +1,5 @@ { - "version": "4.0", + "version": "3.x", "repo": "nette/http", "composer": "nette/http" } diff --git a/latte/cs/@home.texy b/latte/cs/@home.texy deleted file mode 100644 index 7cae06fabb..0000000000 --- a/latte/cs/@home.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{maintitle: Latte – nejbezpečnější & opravdu intuitivní šablony pro PHP}} -{{description: Latte je nejbezpečnější šablonovací systém pro PHP. Zabraňuje spoustě bezpečnostních zranitelností. Oceníte jeho intuitivní syntaxi a oceníte spoustu užitečných vychytávek.}} diff --git a/latte/cs/@left-menu.texy b/latte/cs/@left-menu.texy deleted file mode 100644 index 9c119e9632..0000000000 --- a/latte/cs/@left-menu.texy +++ /dev/null @@ -1,25 +0,0 @@ -- [Začínáme s Latte |guide] -- Koncepty - - [Bezpečnost především |safety-first] - - [Dědičnost šablon |Template Inheritance] - - [Typový systém |type-system] - - [Sandbox] - -- Pro designéry - - [Syntaxe |syntax] - - [Tagy |tags] - - [Filtry |filters] - - [Funkce |functions] - - [Tipy a triky |recipes] - -- Pro vývojáře - - [Vývojářské postupy |develop] - - [Rozšiřujeme Latte |extending-latte] - - [Vytváříme Extension |creating-extension] - -- [Návody a postupy |cookbook/@home] - - [Migrace z Twigu |cookbook/migration-from-twig] - - [Migrace z Latte 2 |cookbook/migration-from-latte2] - - [… další |cookbook/@home] - -- "Hřiště .[link-external]":https://fiddle.nette.org/latte/ .{padding-top:1em} diff --git a/latte/cs/@menu.texy b/latte/cs/@menu.texy deleted file mode 100644 index bf9ac4aa30..0000000000 --- a/latte/cs/@menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -<ul> -- [Úvod |@home] -- [Dokumentace |guide] -- "GitHub .[link-external]":https://github.com/nette/latte -<li class="dropdown"><a class="dropdown-toggle" href="#">Nástroje</a> - <ul class="dropdown-flyout"> -- [fiddle |https://fiddle.nette.org] -- [php2Latte |https://php2latte.nette.org] -- [twig2Latte|https://twig2latte.nette.org] - </ul> -</li> -</ul> diff --git a/latte/cs/cookbook/@home.texy b/latte/cs/cookbook/@home.texy deleted file mode 100644 index 809774dfb5..0000000000 --- a/latte/cs/cookbook/@home.texy +++ /dev/null @@ -1,11 +0,0 @@ -Návody a postupy -**************** - -- [Všechno, co jste kdy chtěli vědět o {iterateWhile} |iteratewhile] -- [Jak psát SQL queries v Latte? |how-to-write-sql-queries-in-latte] -- [Migrace z Latte 2 |migration-from-latte2] -- [Migrace z PHP |migration-from-php] -- [Migrace z Twigu |migration-from-twig] -- [Použití Latte se Slim 4 |slim-framework] - -{{leftbar: /@left-menu}} diff --git a/latte/cs/cookbook/how-to-write-sql-queries-in-latte.texy b/latte/cs/cookbook/how-to-write-sql-queries-in-latte.texy deleted file mode 100644 index bdfc0feedc..0000000000 --- a/latte/cs/cookbook/how-to-write-sql-queries-in-latte.texy +++ /dev/null @@ -1,43 +0,0 @@ -Jak psát SQL queries v Latte? -***************************** - -.[perex] -Latte se může hodit i pro generování opravdu složitých SQL dotazů. - -Pokud vytvoření SQL dotazu obsahuje řadu podmínek a proměnných, může být opravdu přehlednější ho napsat v Latte. Velmi jednoduchý příklad: - -```latte -SELECT users.* FROM users - LEFT JOIN users_groups ON users.user_id = users_groups.user_id - LEFT JOIN groups ON groups.group_id = users_groups.group_id - {ifset $country} LEFT JOIN country ON country.country_id = users.country_id {/ifset} -WHERE groups.name = 'Admins' {ifset $country} AND country.name = {$country} {/ifset} -``` - -Pomocí `$latte->setContentType()` řekneme Latte, aby k obsahu přistupovalo jako k prostému textu (nikoliv jako k HTML) a dále -připravíme escapovací funkci, která bude řetězce escapovat přímo databázovým driverem: - -```php -$db = new PDO(/* ... */); - -$latte = new Latte\Engine; -$latte->setContentType(Latte\ContentType::Text); -$latte->addFilter('escape', fn($val) => match (true) { - is_string($val) => $db->quote($val), - is_int($val), is_float($val) => (string) $val, - is_bool($val) => $val ? '1' : '0', - is_null($val) => 'NULL', - default => throw new Exception('Unsupported type'), -}); -``` - -Použití by vypadalo takto: - -```php -$sql = $latte->renderToString('query.sql.latte', ['country' => $country]); -$result = $db->query($sql); -``` - -*Uvedený příklad vyžaduje Latte v3.0.5 nebo vyšší.* - -{{leftbar: /@left-menu}} diff --git a/latte/cs/cookbook/iteratewhile.texy b/latte/cs/cookbook/iteratewhile.texy deleted file mode 100644 index d224273408..0000000000 --- a/latte/cs/cookbook/iteratewhile.texy +++ /dev/null @@ -1,214 +0,0 @@ -Všechno, co jste kdy chtěli vědět o {iterateWhile} -************************************************** - -.[perex] -Značka `{iterateWhile}` se hodí na nejrůznější kejkle ve foreach cyklech. - -Dejme tomu, že máme následující databázovou tabulku, kde jsou položky rozdělené do kategorií: - -| id | catId | name -|------------------ -| 1 | 1 | Apple -| 2 | 1 | Banana -| 3 | 2 | PHP -| 4 | 3 | Green -| 5 | 3 | Red -| 6 | 3 | Blue - -Vykreslit položky ve foreach cyklu jako seznam je samozřejmě snadné: - -```latte -<ul> -{foreach $items as $item} - <li>{$item->name}</li> -{/foreach} -</ul> -``` - -Ale co kdybychom chtěli, aby každá kategorie byla v samostatném seznamu? Jinými slovy, řešíme úkol, jak seskupit položky v lineárním seznamu ve foreach cyklu do skupin. Výstup by měl vypadat takto: - -```latte -<ul> - <li>Apple</li> - <li>Banana</li> -</ul> - -<ul> - <li>PHP</li> -</ul> - -<ul> - <li>Green</li> - <li>Red</li> - <li>Blue</li> -</ul> -``` - -Rovnou si ukážeme, jak snadno a elegantně se dá úkol vyřešit pomocí iterateWhile: - -```latte -{foreach $items as $item} - <ul> - {iterateWhile} - <li>{$item->name}</li> - {/iterateWhile $item->catId === $iterator->nextValue->catId} - </ul> -{/foreach} -``` - -Zatímco `{foreach}` označuje vnější část cyklu, tedy vykreslování seznamů pro každou kategorii, tak značka `{iterateWhile}` označuje vnitřní část, tedy jednotlivé položky. -Podmínka v koncové značce říká, že opakování bude probíhat do té doby, dokud aktuální i následující prvek patří do stejné kategorie (`$iterator->nextValue` je [následující položka|/tags#$iterator]). - -Kdyby podmínka byla splněná vždy, tak se ve vnitřním cyklu vykreslí všechny prvky: - -```latte -{foreach $items as $item} - <ul> - {iterateWhile} - <li>{$item->name} - {/iterateWhile true} - </ul> -{/foreach} -``` - -Výsledek bude vypadat takto: - -```latte -<ul> - <li>Apple</li> - <li>Banana</li> - <li>PHP</li> - <li>Green</li> - <li>Red</li> - <li>Blue</li> -</ul> -``` - -K čemu je takové použití iterateWhile dobré? Jak se liší od řešení, které jsme si ukázali úplně na začátku tohoto návodu? Rozdíl je v tom, že když bude tabulka prázdná a nebude obsahovat žádné prvky, nevypíše se prázdné `<ul></ul>`. - - -Řešení bez `{iterateWhile}` ---------------------------- - -Pokud bychom stejný úkol řešili zcela základními prostředky šablonovacích systému, například v Twig, Blade, nebo čistém PHP, vypadalo by řešení cca takto: - -```latte -{var $prevCatId = null} -{foreach $items as $item} - {if $item->catId !== $prevCatId} - {* změnila se kategorie *} - - {* uzavřeme předchozí <ul>, pokud nejde o první položku *} - {if $prevCatId !== null} - </ul> - {/if} - - {* otevřeme nový seznam *} - <ul> - - {do $prevCatId = $item->catId} - {/if} - - <li>{$item->name}</li> -{/foreach} - -{if $prevCatId !== null} - {* uzavřeme poslední seznam *} - </ul> -{/if} -``` - -Tento kód je však nesrozumitelný a neintuitivní. Není vůbec jasná vazba mezi otevíracími a zavíracími HTML značkami. Není na první pohled vidět, jestli tam není nějaká chyba. A vyžaduje pomocné proměnné jako `$prevCatId`. - -Oproti tomu řešení s `{iterateWhile}` je čisté, přehledné, nepotřebujeme pomocné proměnné a je blbuvzdorné. - - -Podmínka v otevírací značce ---------------------------- - -Pokud uvedeme podmínku v otevírací značce `{iterateWhile}`, tak se chování změní: podmínka (a přechod na další prvek) se vykoná už na začátku vnitřního cyklu, nikoliv na konci. -Tedy zatímco do `{iterateWhile}` bez podmínky se vstoupí vždy, do `{iterateWhile $cond}` jen při splnění podmínky `$cond`. A zároveň se s tím do `$item` zapíše následující prvek. - -Což se hodí například v situaci, kdy budeme chtít první prvek v každé kategorii vykreslit jiným způsobem, například takto: - -```latte -<h1>Apple</h1> -<ul> - <li>Banana</li> -</ul> - -<h1>PHP</h1> -<ul> -</ul> - -<h1>Green</h1> -<ul> - <li>Red</li> - <li>Blue</li> -</ul> -``` - -Původní kód upravíme tak, že nejprve vykreslíme první položku a poté ve vnitřním cyklu `{iterateWhile}` vykreslíme další položky ze stejné kategorie: - -```latte -{foreach $items as $item} - <h1>{$item->name}</h1> - <ul> - {iterateWhile $item->catId === $iterator->nextValue->catId} - <li>{$item->name}</li> - {/iterateWhile} - </ul> -{/foreach} -``` - - -Vnořené smyčky --------------- - -V rámci jednoho cyklu můžeme vytvářet více vnitřních smyček a dokonce je zanořovat. Takto by se daly seskupovat třeba podkategorie atd. - -Dejme tomu, že v tabulce bude ještě další sloupec `subCatId` a kromě toho, že každá kategorie bude v samostatném `<ul>`, každá každý podkategorie samostatném `<ol>`: - -```latte -{foreach $items as $item} - <ul> - {iterateWhile} - <ol> - {iterateWhile} - <li>{$item->name} - {/iterateWhile $item->subCatId === $iterator->nextValue->subCatId} - </ol> - {/iterateWhile $item->catId === $iterator->nextValue->catId} - </ul> -{/foreach} -``` - - -Filtr |batch ------------- - -Seskupování lineárních položek obstarává také filtr `batch`, a to do dávek s pevným počtem prvků: - -```latte -<ul> -{foreach ($items|batch:3) as $batch} - {foreach $batch as $item} - <li>{$item->name}</li> - {/foreach} -{/foreach} -</ul> -``` - -Lze jej nahradit za iterateWhile tímto způsobem: - -```latte -<ul> -{foreach $items as $item} - {iterateWhile} - <li>{$item->name}</li> - {/iterateWhile $iterator->counter0 % 3} -{/foreach} -</ul> -``` - -{{leftbar: /@left-menu}} diff --git a/latte/cs/cookbook/migration-from-latte2.texy b/latte/cs/cookbook/migration-from-latte2.texy deleted file mode 100644 index 522d6d3070..0000000000 --- a/latte/cs/cookbook/migration-from-latte2.texy +++ /dev/null @@ -1,285 +0,0 @@ -Migrace z Latte v2 na v3 -************************ - -.[perex] -Latte 3 má kompletně přepsaným kompilátor a formálně přesně definovanou gramatiku. Ta by měla co nejvíce odpovídat Latte 2, ale existují konstrukce, které je třeba drobně upravit. - -V praxi se ukazuje, že naprostou většinu šablon není potřeba nijak upravovat a fungují stejně v Latte 2 jako ve verzi 3. Jak ale odhalit nekompatibility? - -**Nejprve si nainstalujte přechodovou verzi Latte 2.11.** - -Tato verze nepřináší žádné novinky, jen pomocí E_USER_DEPRECATED upozorňuje na případy, u kterých ví, že je nové Latte nebude podporovat, a hlavně poradí, jak je upravit. Projít všechny šablony a otestovat, jestli jsou kompatibilní, vám pomůže nástroj [Linter|/develop#linter], který spustíte z konzole: - -```shell -vendor/bin/latte-lint <cesta> -``` - -Když vyřešíte možné nekompatibility, aktualizujte na Latte 3.0. **A opět pusťte Linter**, abyste se ujistili, že nový striktní parser všem šablonám opravdu rozumí. - - -Změny v API -=========== - -Změny v API se týkají jen přidávaní vlastních značek. Zbytek API zůstává stejný jako u verze 2, tj. stejným způsobem se vykreslují šablony, předávají parametry, registrují filtry. - -Výjimkou je nahrazení tzv. dynamického filtru `Engine::addFilter(null, ...)` za [zavaděč filtrů |/extending-latte#Zavaděč filtrů], který si liší tím, že vrací vždy callable a registruje se metodou `Engine::addFilterLoader()`. - -API pro přidávání vlastních značek je úplně jiné, takže doplňky určené pro Latte 2 s ním nebudou fungovat. Dále viz [#aktualizace doplňků]. - - -Změny syntaxe -============= - -Změny jsou následující: - -- ve filtrech se jako oddělovač parametrů používá čárka, dříve `|filtr: arg : arg` je nyní `|filtr: arg, arg` -- značka `{label foo}...{/label}` je vždy párová, nepárovou je potřeba psát `{label /}` -- naopak značka `{_'text'}` je vždy nepárová, párovou `{_}...{/}` nahrazuje nové `{translate}...{/translate}` -- pseudořetězce jako `{block foo-$var}` je potřeba psát v uvozovkách `{block "foo-$var"}` nebo doplnit složené závorky `{block foo-{$var}}` -- to se týká i atributů, tj. místo `n:block="foo-$var"` použijte `n:block="foo-{$var}"`. -- je třeba dodržet velikost písmenek u filtrů, které jsou v Latte 3 case sensitive -- značka `{do ...}` nebo `{php ...}` může obsahovat pouze výrazy, pro použití libovolného PHP zaregistrujte [RawPhpExtension |/develop#RawPhpExtension]. - -A ještě okrajové případy: - -- atributy `n:inner-xxx`, `n:tag-xxx` a `n:ifcontent` nelze použít u nepárových HTML elementů -- atribut `n:inner-snippet` musí být psán bez inner- -- musí být ukončené značky `</script>` a `</style>` -- "odstranění magické proměnné `$iterations`":https://forum.nette.org/cs/35217-latte-3-magicka-promenna-iterations (neplést s `$iterator`!) -- značku `{includeblock file.latte}` nahrazuje [`{include file.latte with blocks}`|/tags#include] nebo [`{import}`|/template-inheritance#horizontální znovupoužití] -- `{include "abc"}` by mělo být psáno jako `{include file "abc"}`, pokud `"abc"` neobsahuje tečku a není tak jasné, že jde o soubor - - -Aktualizace doplňků -=================== - -S kompletním přepsáním parseru se zcela změnil i způsob psaní vlastních značek. Pokud máte pro Latte vytvořené vlastní značky, bude třeba je napsat znovu pro verzi 3, viz [dokumentace|/creating-extension]. - -Pokud používáte cizí doplněk, který přidává značky, je potřeba počkat, až autor vydá verzi pro Latte 3. Knihovny `nette/application`, `nette/caching` a `nette/forms` ve verzi 3.1 a také Texy již aktualizovány jsou a fungují jak s Latte 2, tak i 3. - - -nette/application ------------------ - -.[note] -Při obvyklém použití Nette se toto rozšíření nastavuje automaticky a není třeba nic měnit. - -Starý kód pro Latte 2: - -```php -$latte->onCompile[] = function ($latte) { - Nette\Bridges\ApplicationLatte\UIMacros::install($latte->getCompiler()); -}; - -$latte->addProvider('uiControl', $control); -$latte->addProvider('uiPresenter', $control->getPresenter()); -``` - -Nový kód pro Latte 3: - -```php -$latte->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension($control)); -``` - -UIExtension přidává značky `n:href`, `{link}`, `{control}`, `{snippet}`, atd. Značky pro snippety se tak přesouvají ze samotného Latte do knihovny `nette/application`. V Latte 3 se už nevolá metoda presenteru `templatePrepareFilters()`. - - -nette/forms ------------ - -.[note] -Při obvyklém použití Nette se toto rozšíření nastavuje automaticky a není třeba nic měnit. - -Starý kód pro Latte 2: - -```php -$latte->onCompile[] = function ($latte) { - Nette\Bridges\FormsLatte\FormMacros::install($latte->getCompiler()); -}; -``` - -Nový kód pro Latte 3: - -```php -$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); -``` - - -nette/caching -------------- - -.[note] -Při obvyklém použití Nette se toto rozšíření nastavuje automaticky a není třeba nic měnit. - -Starý kód pro Latte 2: - -```php -$latte->onCompile[] = function ($latte) { - $latte->getCompiler()->addMacro('cache', new Nette\Bridges\CacheLatte\CacheMacro); -}; - -$latte->addProvider('cacheStorage', $cacheStorage); -``` - -Nový kód pro Latte 3: - -```php -$latte->addExtension(new Nette\Bridges\CacheLatte\CacheExtension($cacheStorage)); -``` - - -Překlady --------- - -TranslatorExtension přidává značky pro překlad `{_'text'}`, nové párové `{translate}...{/translate}` a filtr `|translate`. - -Starý kód pro Latte 2: - -```php -$latte->addFilter('translate', [$translator, 'translate']); -``` - -Nový kód pro Latte 3: - -```php -$latte->addExtension(new Latte\Essential\TranslatorExtension($translator)); -``` - -V presenterech se automaticky aktivuje tím, že nastavíte šabloně translator metodou `$template->setTranslator($translator)`. Bez toho značky pro překlad nebudou k dispozici a je potřeba rozšíření zaregistrovat ručně, nebo pomocí konfiguračního souboru. - - -Konfigurační soubor -=================== - -V Latte 2 bylo možné registrovat nové tagy pomocí [konfiguračního souboru |application:configuration#Šablony Latte] v sekci `latte › macros`. Ve verzi 3 se přidávají celé rozšíření tímto způsobem: - -```neon -latte: - extensions: - - App\Templating\LatteExtension - - Latte\Essential\TranslatorExtension -``` - - -Vyvíjíte doplněk pro Latte? -=========================== - -Můžete ve své knihovně mít současně podporu obou verzí Latte. K detekci verze je nejlepší použít konstantu `Latte\Engine::VERSION` a oddělit tak použití `onCompile[]` a `addMacro()` od nového `addExtension()`: - -```php -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - // inicializace Latte 2 - $this->latte->onCompile[] = function ($latte) { - $latte->addMacro(/* ... */); - }; -} else { - // inicializace Latte 3 - $this->latte->addExtension(/* ... */); -} -``` - -Jako příklad zkusíme přepsat následující kód určený pro Latte 2 do podoby pro Latte 3: - -```php -// starý kód pro Latte 2 -$this->latte->onCompile[] = function (Latte\Engine $latte) { - $set = new Latte\Macros\MacroSet($latte->getCompiler()); - $set->addMacro('foo', 'echo %escape(MyClass:myFunc(%node.word, %node.array))'); -}; -``` - -Latte 3 se rozšiřuje pomocí tzv. [extensions|/creating-extension]. Triviální extension přidávající značku `foo` by vypadalo takto: - -```php -// nový kód pro Latte 3 -class FooExtension extends Latte\Extension -{ - public function getTags(): array - { - return [ - 'foo' => [FooNode::class, 'create'], // třídu FooNode doplníme za chvíli - ]; - } -} - -// registrace -$this->latte->addExtension(new FooExtension); -``` - -Nový kompilátor je robustnější, neobsahuje dřívější zkratky, takže napsat makro vyžaduje o něco víc řádků kódu. Nelze například předat přímo řetězec s PHP kódem jako v Latte 2, místo toho vytvoříme funkci. Připomeňme, že v Latte 2 by ona funkce vypadala nějak takto: - -```php -// Latte 2 -$set->addMacro('foo', function (Latte\MacroNode $node, Latte\PhpWriter $writer) { - return $writer->write('echo %escape(MyClass:myFunc(%node.word, %node.array))'); -}); -``` - -Přesto na to Latte 3 jde v podstatě podobně, jen `MacroNode` se jmenuje `Latte\Compiler\Tag` a `PhpWriter` je `Latte\Compiler\PrintContext`. Ale především je tam navíc mezikrok, tedy že funkce nevrací přímo PHP kód, ale vrací uzel, tj. potomka `StatementNode`, který je pak součástí AST stromu. A tento uzel má metodu `print(Latte\Compiler\PrintContext $content): string`, která vrací PHP kód: - -```php -// Latte 3 -class FooNode extends Latte\Compiler\Nodes\StatementNode -{ - public static function create(Latte\Compiler\Tag $tag): self - { - $node = new self; - return $node; // vrací uzel - } - - public function print(Latte\Compiler\PrintContext $context): string - { - return $context->format('echo ...'); // vrací PHP kód - } -} -``` - -Dále maska v `$context->format()` už nemá zkratky `%node.***`, předpokládá se, že obsah značky [nejprve naparsujete |/creating-extension#Parsovací funkce tagu]. Takže využijeme parser a naparsujeme obsah do proměnných (poduzlů), a poté vypíšeme: - -```php -use Latte\Compiler\Nodes\Php\Expression\ArrayNode; -use Latte\Compiler\Nodes\Php\ExpressionNode; - -class FooNode extends Latte\Compiler\Nodes\StatementNode -{ - public ExpressionNode $subject; - public ArrayNode $args; - - public static function create(Latte\Compiler\Tag $tag): self - { - $node = new self; - // parsování obsahu značky - $node->subject = $tag->parser->parseUnquotedStringOrExpression(); - $tag->parser->stream->tryConsume(','); - $node->args = $tag->parser->parseArguments(); - return $node; - } - - public function print(Latte\Compiler\PrintContext $context): string - { - return $context->format( - 'echo %escape(MyClass:myFunc(%node, %node));', - $this->subject, - $this->args, - ); - } -} -``` - -A nakonec doplníme metodu `getIterator()`, aby bylo možné poduzly procházet při tzv. [traversování |/creating-extension#Node Traverser]: - -```php -class FooNode extends Latte\Compiler\Nodes\StatementNode -{ - ... - - public function &getIterator(): \Generator - { - yield $this->subject; - yield $this->args; - } -} -``` - -{{priority: -1}} -{{leftbar: /@left-menu}} diff --git a/latte/cs/cookbook/migration-from-php.texy b/latte/cs/cookbook/migration-from-php.texy deleted file mode 100644 index 4a370b3668..0000000000 --- a/latte/cs/cookbook/migration-from-php.texy +++ /dev/null @@ -1,72 +0,0 @@ -Migrace z PHP do Latte -********************** - -.[perex] -Převádíte starý projekt napsaný v čistém PHP do Latte? Máme pro vás nástroj, které vám migraci usnadní. Vyzkoušejte jej [online |https://php2latte.nette.org]. - -Nástroj si můžete stáhnout z [GitHubu|https://github.com/nette/latte-tools] nebo nainstalovat pomocí Composeru: - -```shell -composer create-project latte/tools -``` - -Převodník nepoužívá jednoduché záměny pomocí regulárních výrazů, naopak využívá přímo PHP parser, takže si poradí s jakkoliv složitou syntaxí. - -K převodu z PHP do Latte slouží skript `php-to-latte.php`: - -```shell -php-to-latte.php input.php [output.latte] -``` - - -Příklad -------- - -Vstupní soubor může vypadat třeba takto (jde o část kódu fóra PunBB): - -```php -<h1><span><?= $lang_common['User list'] ?></span></h1> - -<div class="blockform"> - <form id="userlist" method="get" action="userlist.php"> - <div class="infldset"> -<?php -foreach ($result as $cur_group) { - if ($cur_group['g_id'] == $show_group) { - echo "\n\t\t" . '<option value="' . $cur_group['g_id'] . '" selected="selected">' - . htmlspecialchars($cur_group['g_title']) . '</option>'; - } else { - echo "\n\t\t" . '<option value="' . $cur_group['g_id'] . '">' - . htmlspecialchars($cur_group['g_title']) . '</option>'; - } -} -?> - </select> - <p class="clearb"><?= $lang_ul['User search info'] ?></p> - </div> - </form> -</div> -``` - -Vygeneruje tuto šablonu: - -```latte -<h1><span>{$lang_common['User list']}</span></h1> - -<div class="blockform"> - <form id="userlist" method="get" action="userlist.php"> - <div class="infldset"> -{foreach $result as $cur_group} - {if $cur_group[g_id] == $show_group} - <option value="{$cur_group[g_id]}" selected="selected">{$cur_group[g_title]}</option> - {else} - <option value="{$cur_group[g_id]}">{$cur_group[g_title]}</option> - {/if} -{/foreach} </select> - <p class="clearb">{$lang_ul['User search info']}</p> - </div> - </form> -</div> -``` - -{{leftbar: /@left-menu}} diff --git a/latte/cs/cookbook/migration-from-twig.texy b/latte/cs/cookbook/migration-from-twig.texy deleted file mode 100644 index 36f685747d..0000000000 --- a/latte/cs/cookbook/migration-from-twig.texy +++ /dev/null @@ -1,81 +0,0 @@ -Migrace z Twigu do Latte -************************ - -.[perex] -Převádíte projekt napsaný v Twigu do modernějšího Latte? Máme pro vás nástroj, které vám migraci usnadní. Vyzkoušejte jej [online |https://twig2latte.nette.org]. - -Nástroj si můžete stáhnout z [GitHubu|https://github.com/nette/latte-tools] nebo nainstalovat pomocí Composeru: - -```shell -composer create-project latte/tools -``` - -Převodník nepoužívá jednoduché záměny pomocí regulárních výrazů, naopak využívá přímo Twig parser, takže si poradí s jakkoliv složitou syntaxí. - -K převodu z Twigu do Latte slouží skript `twig-to-latte.php`: - -```shell -twig-to-latte.php input.twig.html [output.latte] -``` - - -Konverze --------- - -Převod předpokládá ruční úpravu výsledku, protože konverzi nelze provést jednoznačně. Twig používá tečkovou syntax, kde `{{ a.b }}` může znamenat `$a->b`, `$a['b']` nebo `$a->getB()`, což nelze rozlišit při kompilaci. Převaděč proto vše převádí na `$a->b`. - -Některé funkce, filtry nebo tagy nemají obdobu v Latte, nebo se mohou chovat mírně jinak. - - -Příklad -------- - -Vstupní soubor může vypadat třeba takto: - -```latte -{% use "blocks.twig" %} -<!DOCTYPE html> -<html> - <head> - <title>{{ block("title") }} - - -

    {% block title %}My Web{% endblock %}

    - - - -``` - -Po konverzi do Latte získáme tuto šablonu: - -```latte -{import 'blocks.latte'} - - - - {include title} - - -

    {block title}My Web{/block}

    - - - -``` - -{{leftbar: /@left-menu}} diff --git a/latte/cs/cookbook/slim-framework.texy b/latte/cs/cookbook/slim-framework.texy deleted file mode 100644 index 217b6f12d6..0000000000 --- a/latte/cs/cookbook/slim-framework.texy +++ /dev/null @@ -1,162 +0,0 @@ -Použití Latte se Slim 4 -*********************** - -.[perex] -Tento článek, jehož autorem je "Daniel Opitz":https://odan.github.io/2022/04/06/slim4-latte.html, popisuje použití Latte se Slim Frameworkem. - -Nejprve si "nainstalujte Slim Framework":https://odan.github.io/2019/11/05/slim4-tutorial.html a poté Latte pomocí Composeru: - -```shell -composer require latte/latte -``` - - -Konfigurace ------------ - -V kořenovém adresáři projektu vytvořte nový adresář `templates`. Všechny šablony do něj budou umístěny později. - -Do souboru `config/defaults.php` přidejte nový konfigurační klíč `template`: - -```php -$settings['template'] = __DIR__ . '/../templates'; -``` - -Latte zkompiluje šablony do nativního kódu PHP a uloží je do mezipaměti na disku. Jsou tedy stejně rychlé, jako kdyby byly napsány v nativním jazyce PHP. - -Do souboru `config/defaults.php` přidejte nový konfigurační klíč `template_temp`: Ujistěte se, že adresář `{project}/tmp/templates` existuje a má práva pro čtení a zápis. - -```php -$settings['template_temp'] = __DIR__ . '/../tmp/templates'; -``` - -Latte automaticky regeneruje mezipaměť při každé změně šablony, což lze v produkčním prostředí vypnout a ušetřit tak trochu výkonu: - -```php -// v produkčním prostředí změňte na false -$settings['template_auto_refresh'] = true; -``` - -Dále přidejte definici kontejneru DI pro třídu `Latte\Engine`. - -```php - function (ContainerInterface $container) { - $latte = new Engine(); - $settings = $container->get('settings'); - $latte->setLoader(new FileLoader($settings['template'])); - $latte->setTempDirectory($settings['template_temp']); - $latte->setAutoRefresh($settings['template_auto_refresh']); - - return $latte; - }, -]; -``` - -Samotné vykreslení šablony Latte by technicky fungovalo, ale musíme také zajistit, aby fungovalo s objektem response PSR-7. - -Za tímto účelem vytvoříme speciální třídu `TemplateRenderer`, která tuto práci udělá za nás. - -Dále tedy vytvořte soubor `src/Renderer/TemplateRenderer.php` a zkopírujte/vložte tento kód: - -```php -engine = $engine; - } - - public function template( - ResponseInterface $response, - string $template, - array $data = [] - ): ResponseInterface - { - $string = $this->engine->renderToString($template, $data); - $response->getBody()->write($string); - - return $response; - } -} -``` - - -Použití -------- - -Místo přímého použití objektu Latte Engine použijeme k vykreslení šablony do objektu kompatibilního s PSR-7 objekt `TemplateRenderer`. - -Typická třída obsluhy akce může vypadat takto: Vykreslí šablonu s názvem `home.latte`: - -```php -renderer = $renderer; - } - - public function __invoke( - ServerRequestInterface $request, - ResponseInterface $response - ): ResponseInterface - { - $viewData = [ - 'items' => ['one', 'two', 'three'], - ]; - - return $this->renderer->template($response, 'home.latte', $viewData); - } -} -``` - -Aby to fungovalo, vytvořte soubor šablony v `templates/home.latte` s tímto obsahem: - -```latte -
      - {foreach $items as $item} -
    • {$item|capitalize}
    • - {/foreach} -
    -``` - -Pokud je vše správně nakonfigurováno, měl by se zobrazit následující výstup: - -```latte -One -Two -Three -``` - -{{priority: -1}} -{{leftbar: /@left-menu}} diff --git a/latte/cs/creating-extension.texy b/latte/cs/creating-extension.texy deleted file mode 100644 index 365a656e46..0000000000 --- a/latte/cs/creating-extension.texy +++ /dev/null @@ -1,579 +0,0 @@ -Vytváříme Extension -******************* - -.[perex]{data-version:3.0} -Tzv. rozšíření je opakovatelně použitelná třída, která může definovat vlastní značky, filtry, funkce, providery, atd. - -Rozšíření vytváříme tehdy, pokud chceme své úpravy Latte znovu použít v různých projektech nebo je sdílet s ostatními. -Je užitečné vytvářet rozšíření i pro každý webový projekt, které bude obsahovat všechny specifické značky a filtry, které chcete v šablonách projektu využívat. - - -Třída rozšíření -=============== - -Rozšíření je třída dědící od [api:Latte\Extension]. Do Latte se registruje pomocí `addExtension()` (případně [konfiguračním souborem |application:configuration#Šablony Latte]): - -```php -$latte = new Latte\Engine; -$latte->addExtension(new MyLatteExtension); -``` - -Pokud zaregistrujete vícero rozšíření a ty definují stejně pojmenované tagy, filtry nebo funkce, vyhrává poslední přidané rozšíření. Z toho také plyne, že vaše rozšíření mohou přepisovat nativní značky/filtry/funkce. - -Kdykoliv v třídě provedete změnu a není vypnutý auto-refresh, Latte automaticky překompiluje vaše šablony. - -Třída může implementovat kteroukoliv z následujících metod: - -```php -abstract class Extension -{ - /** - * Inicializace před kompilací šablony. - */ - public function beforeCompile(Engine $engine): void; - - /** - * Vrací seznam parserů pro značky Latte. - * @return array - */ - public function getTags(): array; - - /** - * Vrací seznam průchodů kompilátoru. - * @return array - */ - public function getPasses(): array; - - /** - * Vrací seznam |filtrů. - * @return array - */ - public function getFilters(): array; - - /** - * Vrací seznam funkcí použitých v šablonách. - * @return array - */ - public function getFunctions(): array; - - /** - * Vrací seznam providerů. - * @return array - */ - public function getProviders(): array; - - /** - * Vrací hodnotu pro rozlišení více verzí šablony. - */ - public function getCacheKey(Engine $engine): mixed; - - /** - * Inicializace před vykreslením šablony. - */ - public function beforeRender(Template $template): void; -} -``` - -Pro představu, jak rozšíření vypadá, se podívejte na vestavěné "CoreExtension":https://github.com/nette/latte/blob/master/src/Latte/Essential/CoreExtension.php. - - -beforeCompile(Latte\Engine $engine): void .[method] ---------------------------------------------------- - -Volá se před kompilací šablony. Metodu lze využít např. pro inicializace související s kompilací. - - -getTags(): array .[method] --------------------------- - -Volá se při kompilaci šablony. Vrací asociativní pole *název tagu => callable*, což jsou [#parsovací funkce tagu]. - -```php -public function getTags(): array -{ - return [ - 'foo' => [FooNode::class, 'create'], - 'bar' => [BarNode::class, 'create'], - 'n:baz' => [NBazNode::class, 'create'], - // ... - ]; -} -``` - -Značka `n:baz` představuje ryzí n:atribut, tj. jde o značku, kterou lze zapisovat pouze jako atribut. - -V případě značek `foo` a `bar` Latte samo rozezná, zda jsou párové, a pokud ano, bude je možné automaticky zapisovat i pomocí n:atributů, včetně variant s prefixy `n:inner-foo` a `n:tag-foo`. - -Pořadí provádění takovýchto n:atributů je dáno jejich pořadím v poli vráceném `getTags()`. Tedy `n:foo` se provede vždy před `n:bar`, i kdyby byly atributy v HTML značce uvedeny v opačném pořadí jako `
    `. - -Pokud potřebujete stanovit pořadí n:atributů napříč více rozšířeními, použijte pomocnou metodu `order()`, kde parametr `before` a nebo `after` určuje, před nebo za kterými značkami se daná značka zařadí. - -```php -public function getTags(): array -{ - return [ - 'foo' => self::order([FooNode::class, 'create'], before: 'bar')] - 'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])] - ]; -} -``` - - -getPasses(): array .[method] ----------------------------- - -Volá se při kompilaci šablony. Vrací asociativní pole *název pass => callable*, což jsou funkce představující tzv. [#průchody kompilátoru], které procházejí a modifikujou AST. - -Opět je možné využít pomocnou metodu `order()`. Hodnotou parametrů `before` nebo `after` může být `'*'` s významem před/za všemi. - -```php -public function getPasses(): array -{ - return [ - 'optimize' => [Passes::class, 'optimizePass'], - 'sandbox' => self::order([$this, 'sandboxPass'], before: '*'), - // ... - ]; -} -``` - - -beforeRender(Latte\Engine $engine): void .[method] --------------------------------------------------- - -Volá se před každým vykreslením šablony. Metodu lze využít např. pro inicializaci proměnných používaných při vykreslování. - - -getFilters(): array .[method] ------------------------------ - -Volá se před vykreslením šablony. Vrací [filtry|extending-latte#filtry] jako asociativní pole *název filtru => callable*. - -```php -public function getFilters(): array -{ - return [ - 'batch' => [$this, 'batchFilter'], - 'trim' => [$this, 'trimFilter'], - // ... - ]; -} -``` - - -getFunctions(): array .[method] -------------------------------- - -Volá se před vykreslením šablony. Vrací [funkce|extending-latte#funkce] jako asociativní pole *název funkce => callable*. - -```php -public function getFunctions(): array -{ - return [ - 'clamp' => [$this, 'clampFunction'], - 'divisibleBy' => [$this, 'divisibleByFunction'], - // ... - ]; -} -``` - - -getProviders(): array .[method] -------------------------------- - -Volá se před vykreslením šablony. Vrací pole tzv. providers, což jsou zpravidla objekty, které za běhu využívají tagy. Přistupují k nim přes `$this->global->...`. - -```php -public function getProviders(): array -{ - return [ - 'myFoo' => $this->foo, - 'myBar' => $this->bar, - // ... - ]; -} -``` - - -getCacheKey(Latte\Engine $engine): mixed .[method] --------------------------------------------------- - -Volá se před vykreslením šablony. Vrácená hodnota se stane součástí klíče, jehož hash je obsažen v názvu souboru se zkompilovanou šablonou. Tedy pro různé vrácené hodnoty Latte vygeneruje různé soubory v cache. - - -Jak Latte funguje? -================== - -Pro pochopení toho, jak definovat vlastní tagy nebo compiler passes, je nezbytné porozumět, jak funguje Latte pod kapotou. - -Kompilace šablon v Latte probíhá zjednodušeně takto: - -- Nejprve **lexer** tokenizuje zdrojový kód šablony na malé části (tokeny) pro snadnější zpracování. -- Poté **parser** převede proud tokenů na smysluplný strom uzlů (abstraktní syntaktický strom, AST). -- Nakonec překladač **vygeneruje** z AST třídu PHP, která vykresluje šablonu, a uloží ji do cache. - -Ve skutečnosti je kompilace o něco složitější. Latte **má dva** lexery a parsery: jeden pro HTML šablonu a druhý pro PHP-like kód uvnitř tagů. A také parsování neprobíhá až po tokenizaci, ale lexer i parser běží paralelně ve dvou "vláknech" a koordinují se. Je to raketová věda :-) - -Dále své parsovací rutiny mají i všechny tagy. Když parser narazí na tag, zavolá jeho parsovací funkci (vrací je [Extension::getTags()|#getTags]). -Jejich úkolem je naparsovat argumenty značky a v případě párových značek i vnitřní obsah. Vrací *uzel*, který se stane součástí AST. Podrobně v části [#Parsovací funkce tagu]. - -Když parser dokončí práci, máme kompletní AST reprezentující šablonu. Kořenovým uzlem je `Latte\Compiler\Nodes\TemplateNode`. Jednotlivé uzly uvnitř stromu pak reprezentují nejen tagy, ale i HTML elementy, jejich atributy, všechny výrazy použité uvnitř značek atd. - -Poté přicházejí na řadu tzv. [#Průchody kompilátoru], což jsou funkce (vrací je [Extension::getPasses()|#getPasses]), které modifikují AST. - -Celý proces od načtení obsahu šablony přes parsování až po vygenerování výsledného souboru se dá sekvenčně vykonat tímto kódem, se kterým můžete experimentovat a dumpovat si jednotlivé mezikroky: - -```php -$latte = new Latte\Engine; -$source = $latte->getLoader()->getContent($file); -$ast = $latte->parse($source); -$latte->applyPasses($ast); -$code = $latte->generate($ast, $file); -``` - - -Příklad AST ------------ - -Pro lepší představu o podobě AST přidáváme ukázku. Toto je zdrojová šablona: - -```latte -{foreach $category->getItems() as $item} -
  • {$item->name|upper}
  • - {else} - no items found -{/foreach} -``` - -A toto její reprezentace v podobě AST: - -/--pre -Latte\Compiler\Nodes\TemplateNode( - Latte\Compiler\Nodes\FragmentNode( - - Latte\Essential\Nodes\ForeachNode( - expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode( - object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category') - name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems') - ) - value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') - content: Latte\Compiler\Nodes\FragmentNode( - - Latte\Compiler\Nodes\TextNode(' ') - - Latte\Compiler\Nodes\Html\ElementNode('li')( - content: Latte\Essential\Nodes\PrintNode( - expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode( - object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') - name: Latte\Compiler\Nodes\Php\IdentifierNode('name') - ) - modifier: Latte\Compiler\Nodes\Php\ModifierNode( - filters: - - Latte\Compiler\Nodes\Php\FilterNode('upper') - ) - ) - ) - ) - else: Latte\Compiler\Nodes\FragmentNode( - - Latte\Compiler\Nodes\TextNode('no items found') - ) - ) - ) -) -\-- - - -Vlastní tagy -============ - -K definování nové značky jsou zapotřebí tři kroky: - -- definování [#parsovací funkce tagu] (zodpovědná za parsování tagu do uzlu) -- vytvoření třídy uzlu (zodpovědné za [#generování PHP kódu] a [#procházení AST]) -- registrace tagu pomocí [Extension::getTags()|#getTags] - - -Parsovací funkce tagu ---------------------- - -Parsování tagů ma na starosti jeho parsovací funkce (ta, kterou vrací [Extension::getTags()|#getTags]). Jejím úkolem je naparsovat a zkontrolovat případné argumenty uvnitř značky (k tomu využívá TagParser). -A dále, pokud je značka párová, požádá TemplateParser o naparsování a vrácení vnitřního obsahu. -Funkce vytvoří a vrátí uzel, který je zpravidla potomkem `Latte\Compiler\Nodes\StatementNode`, a ten se stane součástí AST. - -Pro každý uzel si vytváříme třídu, což uděláme teď hned a parsovací funkci do ní elegantně umístíme jako statickou továrnu. Jako příklad si zkusíme vytvořit známý tag `{foreach}`: - -```php -use Latte\Compiler\Nodes\StatementNode; - -class ForeachNode extends StatementNode -{ - // parsovací funkce, která zatím pouze vytváří uzel - public static function create(Latte\Compiler\Tag $tag): self - { - $node = new self; - return $node; - } - - public function print(Latte\Compiler\PrintContext $context): string - { - // kód doplníme později - } - - public function &getIterator(): \Generator - { - // kód doplníme později - } -} -``` - -Parsovací funkci `create()` se předává objekt [api:Latte\Compiler\Tag], který nese základní informace o tagu (jestli jde o klasický tag nebo n:attribut, na jakém řádku se nachází, apod.) a hlavně zpřístupňuje [api:Latte\Compiler\TagParser] v `$tag->parser`. - -Pokud značka musí mít argumenty, zkontrolujeme jejich existenci zavoláním `$tag->expectArguments()`. Pro jejich parsování jsou k dispozici metody objektu `$tag->parser`: - -- `parseExpression(): ExpressionNode` pro PHP-like výraz (např. `10 + 3`) -- `parseUnquotedStringOrExpression(): ExpressionNode` pro výraz nebo *unquoted-řetězec* -- `parseArguments(): ArrayNode` obsah pole (např. `10, true, foo => bar`) -- `parseModifier(): ModifierNode` pro modifikátor (např `|upper|truncate:10`) -- `parseType(): ExpressionNode` pro typehint (např. `int|string` nebo `Foo\Bar[]`) - -a dále nízkoúrovňový [api:Latte\Compiler\TokenStream] operující přímo s tokeny: - -- `$tag->parser->stream->consume(...): Token` -- `$tag->parser->stream->tryConsume(...): ?Token` - -Latte drobnými způsoby rozšiřuje syntaxi PHP, například o modifikátory, zkrácené ternání operátory, nebo umožňuje jednoduché alfanumerické řetězce psát bez uvozovek. Proto používáme termím *PHP-like* místo PHP. Tudíž metoda `parseExpression()` naparsuje např. `foo` jako `'foo'`. -Vedle toho *unquoted-řetězec* je speciálním případem řetězce, který také nemusí být v uvozovkách, ale zároveň nemusí být ani alfanumerický. Jde třeba o cestu k souboru ve značce `{include ../file.latte}`. K jeho naparsování slouží metoda `parseUnquotedStringOrExpression()`. - -.[note] -Studium tříd uzlů, které jsou součástí Latte, je nejlepší způsob, jak se naučit všechny podrobnosti o procesu parsování. - -Vraťmě se ke značce `{foreach}`. V ní očekáváme argumenty ve tvaru `výraz + 'as' + druhý výraz` a naparsujeme je následujícícm způsobem: - -```php -use Latte\Compiler\Nodes\StatementNode; -use Latte\Compiler\Nodes\Php\ExpressionNode; -use Latte\Compiler\Nodes\AreaNode; - -class ForeachNode extends StatementNode -{ - public ExpressionNode $expression; - public ExpressionNode $value; - - public static function create(Latte\Compiler\Tag $tag): self - { - $tag->expectArguments(); - $node = new self; - $node->expression = $tag->parser->parseExpression(); - $tag->parser->stream->consume('as'); - $node->value = $parser->parseExpression(); - return $node; - } -} -``` - -Výrazy, které jsme zapsali do proměnných `$expression` a `$value`, představují poduzly. - -.[tip] -Proměnné s poduzly definujte jako **public**, aby je bylo možné případně modifikovat v [dalších krocích zpracování |#Průchody kompilátoru]. Zároveň je nutné je **zpřístupnit** pro [procházení |#Procházení AST]. - -U párových značek, jako je ta naše, musí metoda ještě nechat TemplateParser naparsovat její vnitřek. Tohle obstará `yield`, který vrací dvojici ''[vnitří obsah, koncová značka]''. Vnitřní obsah uložíme do proměnné `$node->content`. - -```php -public AreaNode $content; - -public static function create(Latte\Compiler\Tag $tag): \Generator -{ - // ... - [$node->content, $endTag] = yield; - return $node; -} -``` - -Klíčové slovo `yield` způsobí, že se metoda `create()` přeruší, řízení se vrátí zpátky k TemplateParser, který pokračuje v parsování obsahu dokud nenarazí na koncovou značku. Poté předá řízení zpět do `create()`, která pokračuje od místa, kde skončila. Užitím `yield` metoda automaticky vrací `Generator`. - -Do `yield` lze také předat pole názvů značek, u kterých chceme parsování zastavit, pokud se vyskytnou dříve než koncová značka. To nám pomůže implemenotovat konstrukci `{foreach}...{else}...{/foreach}`. Pokud se objeví `{else}`, obsah za ní naparsujeme do `$node->elseContent`: - -```php -public AreaNode $content; -public ?AreaNode $elseContent = null; - -public static function create(Latte\Compiler\Tag $tag): \Generator -{ - // ... - [$node->content, $nextTag] = yield ['else']; - if ($nextTag?->name === 'else') { - [$node->elseContent] = yield; - } - - return $node; -} -``` - -Vrácením uzlu je parsování tagu dokončeno. - - -Generování PHP kódu -------------------- - -Každý uzel musí implementovat metodu `print()`. Vrací PHP kód vykreslující danou část šablony (runtime kód). Jako parametr se jí předává objekt [api:Latte\Compiler\PrintContext], který má užitečnou metodu `format()` zjednodušující sestavení výsledného kódu. - -Metoda `format(string $mask, ...$args)` akceptuje v masce tyto placeholdery: -- `%node` vypisuje Node -- `%dump` vyexporuje hodnotu do PHP -- `%raw` vloží přímo text bez jakékoliv transformace -- `%args` vypíše ArrayNode jako argumenty volání funkce -- `%line` vypíše komentář s číslem řádku -- `%escape(...)` escapuje obsah -- `%modify(...)` aplikuje modifikátor -- `%modifyContent(...)` aplikuje modifikátor pro bloky - - -Naše funkce `print()` by mohla vypadat takto (pro jednoduchost zanedbáváme `else` větev): - -```php -public function print(Latte\Compiler\PrintContext $context): string -{ - return $context->format( - <<<'XX' - foreach (%node as %node) %line { - %node - } - - XX, - $this->expression, - $this->value, - $this->position, - $this->content, - ); -} -``` - -Proměnnou `$this->position` definuje už třída [api:Latte\Compiler\Node] a nastavuje ji parser. Obsahuje objekt [api:Latte\Compiler\Position] s pozicí tagu ve zdrojovém kódu v podobě čísla řádku a sloupce. - -Runtime kód může využívat pomocné proměnné. Aby nedošlo ke kolizi s proměnnými, které používá samotná šablona, je zvykem je prefixovat znaky `$ʟ__`. - -Může také za běhu využívat libovolné hodnoty, které si do šablony předá v podobě tzv. providerů metodou [Extension::getProviders()|#getProviders]. K nim přistupuje pomocí `$this->global->...`. - - -Procházení AST --------------- - -Aby bylo možné AST strom procházet do hloubky, je nutné implementovat metodu `getIterator()`. Ta zpřístupní poduzly: - -```php -public function &getIterator(): \Generator -{ - yield $this->expression; - yield $this->value; - yield $this->content; - if ($this->elseContent) { - yield $this->elseContent; - } -} -``` - -Všimněte si, že `getIterator()` vrací reference. Právě díky tomu mohou *node visitors* jednotlivé uzly měnit za jiné. - -.[warning] -Pokud má uzel poduzly, je nezbytné tuto metodu implementovat a všechny poduzly zpřístupnit. Jinak by mohla vzniknout bezpečnostní díra. Například režim sandboxu by nebyl schopen kontrolovat poduzly a zajistit, aby v nich nebyly volány nepovolené konstrukce. - -Protože klíčové slovo `yield` musí být přítomno v těle metody i pokud nemá žádné podřízené uzly, zapište ji takto: - -```php -public function &getIterator(): \Generator -{ - if (false) { - yield; - } -} -``` - - -Průchody kompilátoru -==================== - -Průchody kompilátoru jsou funkce, které modifikují AST nebo sbírají v nich informace. Vrací je metoda [Extension::getPasses()|#getPasses]. - - -Node Traverser --------------- - -Nejběžnějším způsobem práce s AST je použití [api:Latte\Compiler\NodeTraverser]: - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; - -$ast = (new NodeTraverser)->traverse( - $ast, - enter: fn(Node $node) => ..., - leave: fn(Node $node) => ..., -); -``` - -Funkce *enter* (tj. node visitor) je volána při prvním setkání s uzlem, ještě před zpracováním jeho poduzlů. Funkce *leave* je volána po návštěvě všech poduzlů. -Běžným postupem je, že funkce *enter* se používá ke shromáždění některých informací a poté funkce *leave* na jejich základě provede úpravy. V době, kdy je volána funkce *leave*, bude již veškerý kód uvnitř uzlu navštíven a potřebné informace shromážděny. - -Jak AST modifikovat? Nejjednodušším způsobem je jednoduše měnit vlastnosti uzlů. Druhým způsobem je uzel zcela nahradit vrácením uzlu nového. Příklad: následující kód změní všechna celá čísla v AST na řetězce (např. 42 se změní na `'42'`). - -```php -use Latte\Compiler\Nodes\Php; - -$ast = (new NodeTraverser)->traverse( - $ast, - leave: function (Node $node) { - if ($node instanceof Php\Scalar\IntegerNode) { - return new Php\Scalar\StringNode((string) $node->value); - } - }, -); -``` - -Modul AST může snadno obsahovat tisíce uzlů a procházení všech uzlů může být pomalé. V některých případech je možné se úplnému procházení vyhnout. - -Pokud ve stromu hledáte všechny uzly `Html\ElementNode`, pak víte, že jakmile jednou uvidíte uzel `Php\ExpressionNode`, nemá smysl kontrolovat také všechny jeho podřízené uzly, protože HTML nemůže být uvnitř ve výrazech. V takovém případě můžete traverseru přikázat, aby do uzlu třídy neprováděl rekurzi: - -```php -$ast = (new NodeTraverser)->traverse( - $ast, - enter: function (Node $node) { - if ($node instanceof Php\ExpressionNode) { - return NodeTraverser::DontTraverseChildren; - } - // ... - }, -); -``` - -Pokud hledáte pouze jeden konkrétní uzel, je také možné po jeho nalezení procházení zcela přerušit. - -```php -$ast = (new NodeTraverser)->traverse( - $ast, - enter: function (Node $node) { - if ($node instanceof Nodes\ParametersNode) { - return NodeTraverser::StopTraversal; - } - // ... - }, -); -``` - - -Pomocníci pro uzly ------------------- - -Třída [api:Latte\Compiler\NodeHelpers] poskytuje některé metody, které mohou najít uzly AST, které buď splňují určitou podmínku atd. Několik příkladů: - -```php -use Latte\Compiler\NodeHelpers; - -// najde všechny uzly prvků HTML -$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode); - -// najde první textový uzel -$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode); - -// převede uzel PHP na skutečnou hodnotu -$value = NodeHelpers::toValue($node); - -// převede statický textový uzel na řetězec -$text = NodeHelpers::toText($node); -``` diff --git a/latte/cs/develop.texy b/latte/cs/develop.texy deleted file mode 100644 index 38f0f45207..0000000000 --- a/latte/cs/develop.texy +++ /dev/null @@ -1,299 +0,0 @@ -Vývojářské postupy -****************** - - -Instalace -========= - -Nejlepší způsob, jak nainstalovat Latte, je pomocí Composeru: - -```shell -composer require latte/latte -``` - -Podporované verze PHP (platí pro poslední setinkové verze Latte): - -| verze | kompatibilní s PHP -|-----------------|------------------- -| Latte 3.0 | PHP 8.0 – 8.2 -| Latte 2.11 | PHP 7.1 – 8.2 -| Latte 2.8 – 2.10| PHP 7.1 – 8.1 - - -Jak vykreslit šablonu -===================== - -Jak vykreslit šablonu? Stačí k tomu tento jednoduchý kód: - -```php -$latte = new Latte\Engine; -// adresář pro cache -$latte->setTempDirectory('/path/to/tempdir'); - -$params = [ /* proměnné šablony */ ]; -// or $params = new TemplateParameters(/* ... */); - -// kresli na výstup -$latte->render('template.latte', $params); -// kresli do proměnné -$output = $latte->renderToString('template.latte', $params); -``` - -Parametry mohou být pole nebo ještě lépe [objekt|#Parametry jako třída], který zajistí typovou kontrolu a napovídání v editorech. - -.[note] -Ukázky použití najdete také v repozitáři [Latte examples |https://github.com/nette-examples/latte]. - - -Výkon a cache -============= - -Šablony v Latte jsou nesmírně rychlé, Latte je totiž kompiluje přímo do PHP kódu a ukládá do cache na disk. Nemají tedy žádnou režii navíc oproti šablonám psaným v čistém PHP. - -Cache se automaticky regeneruje pokaždé, když změníte zdrojový soubor. Během vývoje si tedy pohodlně editujete šablony v Latte a změny okamžitě vidíte v prohlížeči. Tuto funkci můžete v produkčním prostředí vypnout a ušetřit tím malinko výkonu: - -```php -$latte->setAutoRefresh(false); -``` - -Při nasazení na produkčním serveru může prvotní vygenerování cache, zejména u rozsáhlejších aplikací, pochopitelně chviličku trvat. Latte má vestavěnou prevenci před "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. -Jde o situaci, kdy se sejde větší počet souběžných požadavků, které spustí Latte, a protože cache ještě neexistuje, začaly by ji všechny generovat současně. Což by neúměrně zatížilo server. -Latte je chytré a při více souběžných požadavcích generuje cache pouze první vlákno, ostatní čekají a následně ji využíjí. - - -Parametry jako třída -==================== - -Lepší než předávat proměnné do šablony jako pole je vytvořit si třídu. Získáte tak [typově bezpečný zápis|type-system], [příjemné napovídání v IDE|recipes#Editory a IDE] a cestu pro [registraci filtrů|extending-latte#Filtry pomocí třídy] a [funkcí|extending-latte#Funkce pomocí třídy]. - -```php -class MailTemplateParameters -{ - public function __construct( - public string $lang, - public Address $address, - public string $subject, - public array $items, - public ?float $price = null, - ) {} -} - -$latte->render('mail.latte', new MailTemplateParameters( - lang: $this->lang, - subject: $title, - price: $this->getPrice(), - items: [], - address: $userAddress, -)); -``` - - -Vypnutí auto-escapování proměnné -================================ - -Pokud proměnná obsahuje řetězec v HTML, můžete ji označit tak, aby ji Latte automaticky (a tedy dvojitě) neescapovalo. Vyhnete se tak potřebě uvádět v šabloně `|noescape`. - -Nejjednodušší cestou je řetězec zabalit do objektu `Latte\Runtime\Html`: - -```php -$params = [ - 'articleBody' => new Latte\Runtime\Html($article->htmlBody), -]; -``` - -Latte dále neescapuje všechny objekty, které implementují rozhraní `Latte\HtmlStringable`. Můžete si tak vytvořit vlastní třídu, jejíž metoda `__toString()` bude vracet HTML kód, který se nebude automaticky escapovat: - -```php -class Emphasis extends Latte\HtmlStringable -{ - public function __construct( - private string $str, - ) { - } - - public function __toString(): string - { - return '' . htmlspecialchars($this->str) . ''; - } -} - -$params = [ - 'foo' => new Emphasis('hello'), -]; -``` - -.[warning] -Metoda `__toString` musí vracet korektní HTML a zajistit escapování parametrů, jinak může dojít ke zranitelnosti XSS! - - -Jak rozšířit Latte o filtry, značky atd. -======================================== - -Jak do Latte přidat vlastní filtr, funkci, značku atd? O tom pojednává kapitola [rozšiřujeme Latte |extending-latte]. -Pokud chcete své úpravy znovu použít v různých projektech nebo je sdílet s ostatními, měli byste [vytvořit rozšíření |creating-extension]. - - -Libovolný kód v šabloně `{php ...}` .{data-version:3.0}{toc: RawPhpExtension} -============================================================================= - -Uvnitř značky [`{do}`|tags#do] lze zapisovat pouze PHP výrazy, nemůžete tak třeba vložit konstrukce jako `if ... else` nebo statementy ukončené středníkem. - -Můžete si však zaregistrovat rozšíření `RawPhpExtension`, které přidá značku `{php ...}`, kterou lze vkládat jakýkoliv PHP kód na zodpovědnost autora šablony. - -```php -$latte->addExtension(new Latte\Essential\RawPhpExtension); -``` - - -Překládání v šablonách .{data-version:3.0}{toc: TranslatorExtension} -==================================================================== - -Pomocí rozšíření `TranslatorExtension` přidáte do šablony značky [`{_...}`|tags#_], [`{translate}`|tags#translate] a filtr [`translate`|filters#translate]. Slouží k překládání hodnot nebo částí šablony do jiných jazyků. Jako parametr uvedeme metodu (PHP callable) provádějící překlad: - -```php -class MyTranslator -{ - public function __construct(private string $lang) - {} - - public function translate(string $original): string - { - // z $original vytvoříme $translated dle $this->lang - return $translated; - } -} - -$translator = new MyTranslator($lang); -$extension = new Latte\Essential\TranslatorExtension( - $translator->translate(...), // [$translator, 'translate'] v PHP 8.0 -); -$latte->addExtension($extension); -``` - -Translator se volá za běhu při vykreslování šablony. Latte ovšem umí všechny statické texty překládat už během kompilace šablony. Tím se ušetří výkon, protože každý řetězec se přeloží jen jednou a výsledný překlad se zapíše do zkompilované podoby. V adresáři s cache tak vznikne více zkompilovaných verzí šablony, jedna pro každý jazyk. K tomu stačí pouze uvést jazyk jako druhý parametr: - -```php -$extension = new Latte\Essential\TranslatorExtension( - $translator->translate(...), - $lang, -); -``` - -Statickým textem je myšleno třeba `{_'hello'}` nebo `{translate}hello{/translate}`. Nestatické texty, jako třeba `{_$foo}`, se nadále budou překládat za běhu. - -Překladači lze ze šablony předávat i doplňující parametry pomocí `{_$original, foo: bar}` nebo `{translate foo: bar}`, které získá jako pole `$params`: - -```php -public function translate(string $original, ...$params): string -{ - // $params['foo'] === 'bar' -} -``` - - -Debuggování a Tracy -=================== - -Latte se vám snaží vývoj co nejvíce zpříjemnit. Přímo pro účely debugování existuje trojice značek [`{dump}`|tags#dump], [`{debugbreak}`|tags#debugbreak] a [`{trace}`|tags#trace]. - -Největší komfort získáte, když ještě si nainstalujete skvělý [ladicí nástroj Tracy|tracy:] a aktivujete doplněk pro Latte: - -```php -// zapne Tracy -Tracy\Debugger::enable(); - -$latte = new Latte\Engine; -// aktivuje rozšíření pro Tracy -$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension); -``` - -Nyní se vám budou všechny chyby zobrazovat v přehledné červené obrazovce, včetně chyb v šablonách se zvýrazněním řádku a sloupce ([video|https://github.com/nette/tracy/releases/tag/v2.9.0]). -Zároveň v pravém dolním rohu v tzv. Tracy Baru se objeví záložka pro Latte, kde jsou přehledně vidět všechny vykreslované šablony a jejich vzájemné vztahy (včetně možnosti se do šablony nebo zkompilovaného kódu prokliknout) a také proměnné: - -[* latte-debugging.webp *] - -Jelikož Latte kompiluje šablony do přehledného PHP kódu, můžete je pohodlně ve svém IDE krokovat. - - -Linter: validace syntaxe šablon .{data-version:2.11}{toc: Linter} -================================================================= - -Projít všechny šablony a zkontrolovat, zda neobsahují syntaktické chyby, vám pomůže nástroj Linter. Spouští se z konzole: - -```shell -vendor/bin/latte-lint -``` - -Pokud používáte vlastní značky, vytvořte si také vlastní verzi Linteru, např. `custom-latte-lint`: - -```php -#!/usr/bin/env php -scanDirectory($path); - -$engine = new Latte\Engine; -// tady zaregistruje jednotlivá rozšíření -$engine->addExtension(/* ... */); - -$path = $argv[1]; -$linter = new Latte\Tools\Linter(engine: $engine); -$ok = $linter->scanDirectory($path); -exit($ok ? 0 : 1); -``` - - -Načítání šablon z řetězce -========================= - -Potřebujete načítat šablony z řetězců místo souborů, třeba pro účely testování? Pomůže vám [StringLoader|extending-latte#stringloader]: - -```php -$latte->setLoader(new Latte\Loaders\StringLoader([ - 'main.file' => '{include other.file}', - 'other.file' => '{if true} {$var} {/if}', -])); - -$latte->render('main.file', $params); -``` - - -Exception handler -================= - -Můžete si definovat vlastní obslužný handler pro očekávané výjimky. Předají se mu výjimky vzniklé uvnitř [`{try}`|tags#try] a v [sandboxu|sandbox]. - -```php -$loggingHandler = function (Throwable $e, Latte\Runtime\Template $template) use ($logger) { - $logger->log($e); -}; - -$latte = new Latte\Engine; -$latte->setExceptionHandler($loggingHandler); -``` - - -Automatické dohledávání layoutu -=============================== - -Pomocí značky [`{layout}`|template-inheritance#layoutova-dedicnost] šablona určuje svou rodičovskou šablonu. Je taky možné nechat dohledávat layout automaticky, což zjednoduší psaní šablon, neboť v nich nebude nutné značku `{layout}` uvádět. - -Dosáhne se toho následujícím způsobem: - -```php -$finder = function (Latte\Runtime\Template $template) { - if (!$template->getReferenceType()) { - // vrací cestu k souboru s layoutem - return 'automatic.layout.latte'; - } -}; - -$latte = new Latte\Engine; -$latte->addProvider('coreParentFinder', $finder); -``` - -Pokud šablona nemá mít layout, oznámí to značkou `{layout none}`. diff --git a/latte/cs/extending-latte.texy b/latte/cs/extending-latte.texy deleted file mode 100644 index ab665f3dbd..0000000000 --- a/latte/cs/extending-latte.texy +++ /dev/null @@ -1,289 +0,0 @@ -Rozšiřujeme Latte -***************** - -.[perex] -Latte je velmi flexibilní a lze jej rozšířit mnoha způsoby: můžete přidat vlastní filtry, funkce, značky, loadery atd. Ukážeme si jak na to. - -Tato kapitola popisuje jednotlivé cesty rozšiřování Latte. Pokud chcete své úpravy znovu použít v různých projektech nebo je sdílet s ostatními, měli byste [vytvořit tzv. rozšíření |creating-extension]. - - -Kolik cest vede do Říma? -======================== - -Protože některé způsoby rozšíření Latte mohou splývat, zkusíme si nejprve vysvětlit rozdíly mezi nimi. Jako příklad se pokusíme implementovat generátor *Lorem ipsum*, kterému předáme počet slov, jenž má vygenerovat. - -Hlavní konstrukcí jazyka Latte je značka (tag). Generátor můžeme implementovat rozšířením jazyka Latte o nový tag: - -```latte -{lipsum 40} -``` - -Tag bude skvěle fungovat. Nicméně generátor v podobě tagu nemusí být dostatečně flexibilní, protože jej nelze použít ve výrazu. Mimochodem v praxi potřebujete tagy vytvářet jen zřídka; a to je dobrá zpráva, protože tagy jsou složitějším způsobem rozšíření. - -Dobrá, zkusme místo tagu vytvořit filtr: - -```latte -{=40|lipsum} -``` - -Opět validní možnost. Ale filtr by měl předanou hodnotu transformovat na něco jiného. Zde hodnotu `40`, která udává počet vygenerovaných slov, používáme jako argument filtru, nikoli jako hodnotu, kterou chceme transformovat. - -Tak zkusíme použít funkci: - -```latte -{lipsum(40)} -``` - -To je ono! Pro tento konkrétní příklad je vytvoření funkce ideálním způsobem rozšíření. Můžete ji volat kdekoli, kde je akceptován výraz, například: - -```latte -{var $text = lipsum(40)} -``` - - -Filtry -====== - -Filtr vytvoříme zaregistrováním jeho názvu a libovolného PHP callable, třeba funkce: - -```php -$latte = new Latte\Engine; -$latte->addFilter('shortify', function (string $s): string { - return mb_substr($s, 0, 10); // zkrátí text na 10 písmen -}); -``` - -V tomto případě by bylo šikovnější, kdyby filtr přijímal další parametr: - -```php -$latte->addFilter('shortify', function (string $s, int $len = 10): string { - return mb_substr($s, 0, $len); -}); -``` - -V šabloně se potom volá takto: - -```latte -

    {$text|shortify}

    -

    {$text|shortify:100}

    -``` - -Jak vidíte, funkce obdrží levou stranu filtru před pipe `|` jako první argument a argumenty předané filtru za `:` jako další argumenty. - -Funkce představující filtr může samozřejmě přijímat libovolný počet parametrů, podporovány jsou i variadic parametry. - - -Filtry pomocí třídy -------------------- - -Druhým způsobem definice filtru je [využití třídy|develop#Parametry jako třída]. Vytvoříme metodu s atributem `TemplateFilter`: - -```php -class TemplateParameters -{ - public function __construct( - // parameters - ) {} - - #[Latte\Attributes\TemplateFilter] - public function shortify(string $s, int $len = 10): string - { - return mb_substr($s, 0, $len); - } -} - -$params = new TemplateParameters(/* ... */); -$latte->render('template.latte', $params); -``` - -Pokud používáte PHP 7.x a Latte 2.x, místo atributu uveďte anotaci `/** @filter */`. - - -Zavaděč filtrů .{data-version:2.10} ------------------------------------ - -Místo registrace jednotlivých filtrů lze vytvořit tzv. zavaděč, což je funkce, které se zavolá s názvem filtru jako argumentem a vrátí jeho PHP callable, nebo null. - -```php -$latte->addFilterLoader([new Filters, 'load']); - - -class Filters -{ - public function load(string $filter): ?callable - { - if (in_array($filter, get_class_methods($this))) { - return [$this, $filter]; - } - return null; - } - - public function shortify($s, $len = 10) - { - return mb_substr($s, 0, $len); - } - - // ... -} -``` - - -Kontextové filtry ------------------ - -Kontextový filtr je takový, který v prvním parametru přijímá objekt [api:Latte\Runtime\FilterInfo] a teprve po něm následují další parametry jako u klasických filtrů. Registruje se stejný způsobem, Latte samo rozpozná, že filtr je kontextový: - -```php -use Latte\Runtime\FilterInfo; - -$latte->addFilter('foo', function (FilterInfo $info, string $str): string { - // ... -}); -``` - -Kontextové filtry mohou zjišťovat a měnit content-type, který obdrží v proměnné `$info->contentType`. Pokud se filtr volá klasicky nad proměnnou (např. `{$var|foo}`), bude `$info->contentType` obsahovat null. - -Filtr by měl nejprve ověřit, zda content-type vstupního řetězce podporuje. A může ho také změnit. Příklad filtru, který přijímá text (nebo null) a vrací HTML: - -```php -use Latte\Runtime\FilterInfo; - -$latte->addFilter('money', function (FilterInfo $info, float $amount): string { - // nejprve oveříme, zda je vstupem content-type text - if (!in_array($info->contentType, [null, ContentType::Text])) { - throw new Exception("Filter |money used in incompatible content type $info->contentType."); - } - - // změníme content-type na HTML - $info->contentType = ContentType::Html; - return "$num Kč"; -}); -``` - -.[note] -Filtr musí v takovém případě zajistit správné escapování dat. - -Všechny filtry, které se používají nad [bloky|tags#block] (např. jako `{block|foo}...{/block}`), musí být kontextové. - - -Funkce .{data-version:2.6} -========================== - -V Latte lze standardně používat všechny nativní funkce z PHP, pokud to nezakáže sandbox. Ale zároveň si můžete definovat funkce vlastní. Mohou přepsat funkce nativní. - -Funkci vytvoříme zaregistrováním jejího názvu a libovolného PHP callable: - -```php -$latte = new Latte\Engine; -$latte->addFunction('random', function (...$args) { - return $args[array_rand($args)]; -}); -``` - -Použití je pak stejné, jako když voláte PHP funkci: - -```latte -{random(jablko, pomeranč, citron)} // vypíše například: jablko -``` - - -Funkce pomocí třídy -------------------- - -Druhým způsobem definice funkce je [využití třídy|develop#Parametry jako třída]. Vytvoříme metodu s atributem `TemplateFunction`: - -```php -class TemplateParameters -{ - public function __construct( - // parameters - ) {} - - #[Latte\Attributes\TemplateFunction] - public function random(...$args) - { - return $args[array_rand($args)]; - } -} - -$params = new TemplateParameters(/* ... */); -$latte->render('template.latte', $params); -``` - -Pokud používáte PHP 7.x a Latte 2.x, místo atributu uveďte anotaci `/** @function */`. - - -Loadery -======= - -Loadery jsou zodpovědné za načítání šablon ze zdroje, například ze souborového systému. Nastaví se metodou `setLoader()`: - -```php -$latte->setLoader(new MyLoader); -``` - -Vestavěné loadery jsou tyto: - - -FileLoader ----------- - -Výchozí loader. Načítá šablony ze souborového systému. - -Přístup k souborům je možné omezit nastavením základního adresáře: - -```php -$latte->setLoader(new Latte\Loaders\FileLoader($templateDir)); -$latte->render('test.latte'); -``` - - -StringLoader ------------- - -Načítá šablony z řetězců. Tento loader je velmi užitečný pro testování. Lze jej také použít pro malé projekty, kde může mít smysl ukládat všechny šablony do jediného souboru PHP. - -```php -$latte->setLoader(new Latte\Loaders\StringLoader([ - 'main.file' => '{include other.file}', - 'other.file' => '{if true} {$var} {/if}', -])); - -$latte->render('main.file'); -``` - -Zjednodušené použití: - -```php -$template = '{if true} {$var} {/if}'; -$latte->setLoader(new Latte\Loaders\StringLoader); -$latte->render($template); -``` - - -Vytvoření vlastního loaderu ---------------------------- - -Loader je třída, která implementuje rozhraní [api:Latte\Loader]. - - -Tagy (makra) -============ - -Jednou z nejzajímavějších funkcí šablonovacího jádra je možnost definovat nové jazykové konstrukce pomocí značek. Je to také složitější funkčnost a je třeba pochopit, jak Latte interně funguje. - -Většinou však značka není potřeba: -- pokud by měla generovat nějaký výstup, použijte místo ní [funkci|#funkce] -- pokud měla upravovat nějaký vstup a vracet ho, použijte místo toho [filtr|#filtry] -- pokud měla upravovat oblast textu, obalte jej značkou [`{block}`|tags#block] a použijte [filtr|#Kontextové filtry] -- pokud neměla nic vypisovat, ale pouze volat funkci, zavolejte ji pomocí [`{do}`|tags#do] - -Pokud přesto chcete vytvořit tag, skvěle! Vše podstatné najdete v kapitole [Vytváříme Extension|creating-extension] (týká se Latte 3.x; viz také [podrobný návod pro Latte 2.x|http://css.nothrem.cz/latte-makra/]). - - -Průchody kompilátoru .{data-version:3.0} -======================================== - -Průchody kompilátoru jsou funkce, které modifikují AST nebo sbírají v nich informace. V Latte je takto implementován například sandbox: projde všechny uzly AST, najde volání funkcí a metod a nahradí je za jím kontrolované volání. - -Stejně jako v případě značek se jedná o složitější funkcionalitu a je potřeba pochopit, jak funguje Latte pod kapotou. Vše podstatné najdete v kapitole [Vytváříme Extension|creating-extension]. diff --git a/latte/cs/filters.texy b/latte/cs/filters.texy deleted file mode 100644 index ad781bbafb..0000000000 --- a/latte/cs/filters.texy +++ /dev/null @@ -1,715 +0,0 @@ -Latte filtry -************ - -.[perex] -V šablonách můžeme používat funkce, které pomáhají upravit nebo přeformátovat data do výsledné podoby. Říkáme jim *filtry*. - -.[table-latte-filters] -|## Transformace -| `batch` | [výpis lineárních dat do tabulky |#batch] -| `breakLines` | [Před konce řádku přidá HTML odřádkování |#breakLines] -| `bytes` | [formátuje velikost v bajtech |#bytes] -| `clamp` | [ohraničí hodnotu do daného rozsahu |#clamp] -| `dataStream` | [konverze pro Data URI protokol |#datastream] -| `date` | [formátuje datum |#date] -| `explode` | [rozdělí řetězec na pole podle oddělovače |#explode] -| `first` | [vrací první prvek pole nebo znak řetězce |#first] -| `implode` | [spojí pole do řetězce |#implode] -| `indent` | [odsadí text zleva o daný počet tabulátorů |#indent] -| `join` | [spojí pole do řetězce |#implode] -| `last` | [vrací poslední prvek pole nebo znak řetězce |#last] -| `length` | [vrací délku řetězce ve znacích nebo pole |#length] -| `number` | [formátuje číslo |#number] -| `padLeft` | [doplní řetězec zleva na požadovanou délku |#padLeft] -| `padRight` | [doplní řetězec zprava na požadovanou délku |#padRight] -| `random` | [vrací náhodný prvek pole nebo znak řetězce |#random] -| `repeat` | [opakování řetězce |#repeat] -| `replace` | [zamění výskyty hledaného řetězce |#replace] -| `replaceRE` | [zamění výskyty dle regulárního výrazu |#replaceRE] -| `reverse` | [obrátí UTF‑8 řetězec nebo pole |#reverse] -| `slice` | [extrahuje část pole nebo řetězce |#slice] -| `sort` | [seřadí pole |#sort] -| `spaceless` | [odstraní bílé místo |#spaceless], podobně jako značka [spaceless |tags] tag -| `split` | [rozdělí řetězec na pole podle oddělovače |#explode] -| `strip` | [odstraní bílé místo |#spaceless] -| `stripHtml` | [odstraní HTML značky a HTML entity převede na znaky |#stripHtml] -| `substr` | [vrátí část řetězce |#substr] -| `trim` | [odstraní počáteční a koncové mezery či jiné znaky |#trim] -| `translate` | [překlad do jiných jazyků |#translate] -| `truncate` | [zkrátí délku se zachováním slov |#truncate] -| `webalize` | [upraví UTF‑8 řetězec do tvaru používaného v URL |#webalize] - -.[table-latte-filters] -|## Velikost písmen -| `capitalize` | [malá písmena, první písmeno ve slovech velké |#capitalize] -| `firstUpper` | [převede první písmeno na velké |#firstUpper] -| `lower` | [převede na malá písmena |#lower] -| `upper` | [převede na velká písmena |#upper] - -.[table-latte-filters] -|## Zaokrouhlování -| `ceil` | [zaokrouhlí číslo nahoru na danou přesnost |#ceil] -| `floor` | [zaokrouhlí číslo dolů na danou přesnost |#floor] -| `round` | [zaokrouhlí číslo na danou přesnost |#round] - -.[table-latte-filters] -|## Escapování -| `escapeUrl` | [escapuje parametr v URL |#escapeUrl] -| `noescape` | [vypíše proměnnou bez escapování |#noescape] -| `query` | [generuje query string v URL |#query] - -Dále existují escapovací filtry pro HTML (`escapeHtml` a `escapeHtmlComment`), XML (`escapeXml`), JavaScript (`escapeJs`), CSS (`escapeCss`) a iCalendar (`escapeICal`), které Latte používá samo díky [kontextově sensitivnímu escapování|safety-first#Kontextově sensitivní escapování] a nemusíte je zapisovat. - -.[table-latte-filters] -|## Bezpečnost -| `checkUrl` | [ošetří URL adresu od nebezpečných vstupů |#checkUrl] -| `nocheck` | [předejde automatickému ošetření URL adresy |#nocheck] - -Latte atributy `src` a `href` [kontroluje automaticky |safety-first#Kontrola odkazů], takže filtr `checkUrl` téměř nemusíte používat. - - -.[note] -Všechny výchozí filtry jsou určeny pro řetězce v kódování UTF‑8. - - -Použití -======= - -Filtry se zapisují za svislítko (může být před ním mezera): - -```latte -

    {$heading|upper}

    -``` - -Filtry (ve starších verzích helpery) lze zřetězit a poté se aplikují v pořadí od levého k pravému: - -```latte -

    {$heading|lower|capitalize}

    -``` - -Parametry se zadávají za jménem filtru oddělené dvojtečkami nebo čárkami: - -```latte -

    {$heading|truncate:20,''}

    -``` - -Filtry lze aplikovat i na výraz: - -```latte -{var $name = ($title|upper) . ($subtitle|lower)} -``` - -[Vlastní filtry|extending-latte#filtry] lze registrovat tímto způsobem: - -```php -$latte = new Latte\Engine; -$latte->addFilter('shortify', function (string $s, int $len = 10): string { - return mb_substr($s, 0, $len); -}); -``` - -V šabloně se potom volá takto: - -```latte -

    {$text|shortify}

    -

    {$text|shortify:100}

    -``` - - -Filtry -====== - - -batch(int length, mixed item): array .[filter]{data-version:2.7} ----------------------------------------------------------------- -Filtr, který zjednodušuje výpis lineárních dat do podoby tabulky. Vrací pole polí se zadaným počtem položek. Pokud zadáte druhý parametr, použije se k doplnění chybějících položek na posledním řádku. - -```latte -{var $items = ['a', 'b', 'c', 'd', 'e']} - -{foreach ($items|batch: 3, 'No item') as $row} - - {foreach $row as $column} - - {/foreach} - -{/foreach} -
    {$column}
    -``` - -Vypíše: - -```latte - - - - - - - - - - - -
    abc
    deNo item
    -``` - - -breakLines .[filter] --------------------- -Přidává před každý znak nového řádku HTML značku `
    ` - -```latte -{var $s = "Text & with \n newline"} -{$s|breakLines} {* vypíše "Text & with
    \n newline" *} -``` - - -bytes(int precision = 2) .[filter] ----------------------------------- -Formátuje velikost v bajtech do lidsky čitelné podoby. - -```latte -{$size|bytes} 0 B, 10 B, … -{$size|bytes:2} 1.25 GB, … -``` - - -ceil(int precision = 0) .[filter] ---------------------------------- -Zaokrouhlí číslo nahoru na danou přesnost. - -```latte -{=3.4|ceil} {* vypíše 4 *} -{=135.22|ceil:1} {* vypíše 135.3 *} -{=135.22|ceil:3} {* vypíše 135.22 *} -``` - -Viz také [#floor], [#round]. - - -capitalize .[filter] --------------------- -Slova budou začínat velkými písmeny, všechny zbývající znaky budou malá. Vyžaduje PHP rozšíření `mbstring`. - -```latte -{='i like LATTE'|capitalize} {* vypíše 'I Like Latte' *} -``` - -Viz také [#firstUpper], [#lower], [#upper]. - - -checkUrl .[filter] ------------------- -Vynutí ošetření URL adresy. Kontroluje, zda proměnná obsahuje webovou URL (tj. protokol HTTP/HTTPS) a předchází vypsání odkazů, které mohou představovat bezpečnostní riziko. - -```latte -{var $link = 'javascript:window.close()'} -kontrolované -nekontrolované -``` - -Vypíše: - -```latte -kontrolované -nekontrolované -``` - -Viz také [#nocheck]. - - -clamp(int|float min, int|float max) .[filter]{data-version:2.9} ---------------------------------------------------------------- -Ohraničí hodnotu do daného inkluzivního rozsahu min a max. - -```latte -{$level|clamp: 0, 255} -``` - -Existuje také jako [funkce|functions#clamp]. - - -dataStream(string mimetype = detect) .[filter] ----------------------------------------------- -Konvertuje obsah do data URI scheme. Pomocí něj lze do HTML nebo CSS vkládat obrázky bez nutnosti linkovat externí soubory. - -Mějme v proměnné obrázek `$img = Image::fromFile('obrazek.gif')`, poté - -```latte - -``` - -Vypíše například: - -```latte - -``` - -.[caution] -Vyžaduje PHP rozšíření `fileinfo`. - - -date(string format) .[filter] ------------------------------ -Formátuje datum podle masky buď ve tvaru používaném PHP funkcí [php:strftime] nebo [php:date]. Filtr přijímá datum buď ve formátu UNIX timestamp, v podobě řetězce nebo jako objekt `DateTime`. - -```latte -{$today|date:'%d.%m.%Y'} -{$today|date:'j. n. Y'} -``` - - -escapeUrl .[filter] -------------------- -Escapuje proměnnou pro použití jakožto parametru v URL. - -```latte -{$name} -``` - -Viz také [#query]. - - -explode(string separator = '') .[filter]{data-version:2.10.2} -------------------------------------------------------------- -Rozdělí řetězec na pole podle oddělovače. Alias pro `split`. - -```latte -{='one,two,three'|explode:','} {* vrací ['one', 'two', 'three'] *} -``` - -Pokud je oddělovač prázdný řetězec (výchozí hodnota), bude vstup rozdělen na jednotlivé znaky: - -```latte -{='123'|explode} {* vrací ['1', '2', '3'] *} -``` - -Můžete také použít alias `split`: - -```latte -{='1,2,3'|split:','} {* vrací ['1', '2', '3'] *} -``` - -Viz také [#implode]. - - -first .[filter]{data-version:2.10.2} ------------------------------------- -Vrací první prvek pole nebo znak řetězce: - -```latte -{=[1, 2, 3, 4]|first} {* vypíše 1 *} -{='abcd'|first} {* vypíše 'a' *} -``` - -Viz také [#last], [#random]. - - -floor(int precision = 0) .[filter] ----------------------------------- -Zaokrouhlí číslo dolů na danou přesnost. - -```latte -{=3.5|floor} {* vypíše 3 *} -{=135.79|floor:1} {* vypíše 135.7 *} -{=135.79|floor:3} {* vypíše 135.79 *} -``` - -Viz také [#ceil], [#round]. - - -firstUpper .[filter] --------------------- -Převede první písmeno na velká. Vyžaduje PHP rozšíření `mbstring`. - -```latte -{='the latte'|firstUpper} {* vypíše 'The latte' *} -``` - -Viz také [#capitalize], [#lower], [#upper]. - - -implode(string glue = '') .[filter] ------------------------------------ -Vrátí řetězec, který je zřetězením položek sekvence. Alias pro `join`. - -```latte -{=[1, 2, 3]|implode} {* vypíše '123' *} -{=[1, 2, 3]|implode:'|'} {* vypíše '1|2|3' *} -``` - -Můžete také použít alias `join`: .{data-version:2.10.2} - -```latte -{=[1, 2, 3]|join} {* vypíše '123' *} -``` - - -indent(int level = 1, string char = "\t") .[filter] ---------------------------------------------------- -Odsadí text zleva o daný počet tabulátorů nebo jiných znaků, které můžeme uvést ve druhém argumentu. Prázdné řádky nejsou odsazeny. - -```latte -
    -{block |indent} -

    Hello

    -{/block} -
    -``` - -Vypíše: - -```latte -
    -

    Hello

    -
    -``` - - -last .[filter]{data-version:2.10.2} ------------------------------------ -Vrací poslední prvek pole nebo znak řetězce: - -```latte -{=[1, 2, 3, 4]|last} {* vypíše 4 *} -{='abcd'|last} {* vypíše 'd' *} -``` - -Viz také [#first], [#random]. - - -length .[filter] ----------------- -Vrátí délku řetězce nebo pole. - -- pro řetězce vrátí délku v UTF‑8 znacích -- pro pole vrátí počet položek -- pro objekty, které implementují rozhraní Countable, použije návratovou hodnotu metody count() -- pro objekty, které implementují rozhraní IteratorAggregate, použije návratovou hodnotu funkce iterator_count() - - -```latte -{if ($users|length) > 10} - ... -{/if} -``` - - -lower .[filter] ---------------- -Převede řetězec na malá písmena. Vyžaduje PHP rozšíření `mbstring`. - -```latte -{='LATTE'|lower} {* vypíše 'latte' *} -``` - -Viz také [#capitalize], [#firstUpper], [#upper]. - - -nocheck .[filter] ------------------ -Předejde automatickému ošetření URL adresy. Latte [automaticky kontroluje|safety-first#Kontrola odkazů], zda proměnná obsahuje webovou URL (tj. protokol HTTP/HTTPS) a předchází vypsání odkazů, které mohou představovat bezpečnostní riziko. - -Pokud odkaz používá jiné schéma, např. `javascript:` nebo `data:`, a jste si jistí jeho obsahem, můžete kontrolu vypnout pomoci `|nocheck`. - -```latte -{var $link = 'javascript:window.close()'} - -kontrolované -nekontrolované -``` - -Vypíše: - -```latte -kontrolované -nekontrolované -``` - -Viz také [#checkUrl]. - - -noescape .[filter] ------------------- -Zakáže automatické escapování. - -```latte -{var $trustedHtmlString = 'hello'} -Escapovaný: {$trustedHtmlString} -Neescapovaný: {$trustedHtmlString|noescape} -``` - -Vypíše: - -```latte -Escapovaný: <b>hello</b> -Neescapovaný: hello -``` - -.[warning] -Špatné použití filtru `noescape` může vést ke vzniku zranitelnosti XSS! Nikdy jej nepoužívejte, pokud si nejste **zcela jisti** co děláte, a že vypisovaný řetězec pochází z důvěryhodného zdroje. - - -number(int decimals = 0, string decPoint = '.', string thousandsSep = ',') .[filter] ------------------------------------------------------------------------------------- -Formátuje číslo na určitý počet desetinných míst. Lze určit znak pro desetinnou čárku a oddělovač tisíců. - -```latte -{1234.20 |number} 1,234 -{1234.20 |number:1} 1,234.2 -{1234.20 |number:2} 1,234.20 -{1234.20 |number:2, ',', ' '} 1 234,20 -``` - - -padLeft(int length, string pad = ' ') .[filter] ------------------------------------------------ -Doplní řetězec do určité délky jiným řetězcem zleva. - -```latte -{='hello'|padLeft: 10, '123'} {* vypíše '12312hello' *} -``` - - -padRight(int length, string pad = ' ') .[filter] ------------------------------------------------- -Doplní řetězec do určité délky jiným řetězcem zprava. - -```latte -{='hello'|padRight: 10, '123'} {* vypíše 'hello12312' *} -``` - - -query .[filter]{data-version:2.10} ----------------------------------- -Dynamicky generuje query string v URL: - -```latte -click -search -``` - -Vypíše: - -```latte -click -search -``` - -Klíče s hodnotou `null` se vynechají. - -Viz také [#escapeUrl]. - - -random .[filter]{data-version:2.10.2} -------------------------------------- -Vrací náhodný prvek pole nebo znak řetězce: - -```latte -{=[1, 2, 3, 4]|random} {* vypíše např.: 3 *} -{='abcd'|random} {* vypíše např.: 'b' *} -``` - -Viz také [#first], [#last]. - - -repeat(int count) .[filter] ---------------------------- -Opakuje řetězec x-krát. - -```latte -{='hello'|repeat: 3} {* vypíše 'hellohellohello' *} -``` - - -replace(string|array search, string replace = '') .[filter] ------------------------------------------------------------ -Nahradí všechny výskyty vyhledávacího řetězce náhradním řetězcem. - -```latte -{='hello world'|replace: 'world', 'friend'} {* vypíše 'hello friend' *} -``` - -Lze provést i více záměn najednou: .{data-version:2.10.2} - -```latte -{='hello world'|replace: [h => l, l => h]} {* vypíše 'lehho worhd' *} -``` - - -replaceRE(string pattern, string replace = '') .[filter] --------------------------------------------------------- -Provede vyhledávání regulárních výrazů s nahrazením. - -```latte -{='hello world'|replaceRE: '/l.*/', 'l'} {* vypíše 'hel' *} -``` - - -reverse .[filter] ------------------ -Obrátí daný řetězec nebo pole. - -```latte -{var $s = 'Nette'} -{$s|reverse} {* vypíše 'etteN' *} -{var $a = ['N', 'e', 't', 't', 'e']} -{$a|reverse} {* returns ['e', 't', 't', 'e', 'N'] *} -``` - - -round(int precision = 0) .[filter] ----------------------------------- -Zaokrouhlí číslo na danou přesnost. - -```latte -{=3.4|round} {* vypíše 3 *} -{=3.5|round} {* vypíše 4 *} -{=135.79|round:1} {* vypíše 135.8 *} -{=135.79|round:3} {* vypíše 135.79 *} -``` - -Viz také [#ceil], [#floor]. - - -slice(int start, int length = null, bool preserveKeys = false) .[filter]{data-version:2.10.2} ---------------------------------------------------------------------------------------------- -Extrahuje část pole nebo řetězce. - -```latte -{='hello'|slice: 1, 2} {* vypíše 'el' *} -{=['a', 'b', 'c']|slice: 1, 2} {* vypíše ['b', 'c'] *} -``` - -Filtr funguje jako funkce PHP `array_slice` pro pole nebo `mb_substr` pro řetězce s fallbackem na funkci `iconv_substr` v režimu UTF‑8. - -Pokud je start kladný, posloupnost začné posunutá o tento počet od začátku pole/řetezce. Pokud je záporný posloupnost začné posunutá o tolik od konce. - -Pokud je zadaný parametr length a je kladný, posloupnost bude obsahovat tolik prvků. Pokud je do této funkce předán záporný parametr length, posloupnost bude obsahovat všechny prvky původního pole, začínající na pozici start a končicí na pozici menší na length prvků od konce pole. Pokud tento parametr nezadáte, posloupnost bude obsahovat všechny prvky původního pole, začínající pozici start. - -Ve výchozím nastavení filtr změní pořadí a resetuje celočíselného klíče pole. Toto chování lze změnit nastavením preserveKeys na true. Řetězcové klíče jsou vždy zachovány, bez ohledu na tento parametr. - - -sort .[filter]{data-version:2.9} --------------------------------- -Filtr, který seřadí pole. Zachovává asociaci s klíčí. - -```latte -{foreach ($names|sort) as $name} - ... -{/foreach} -``` - -Řazené pole v opačném pořadí: - -```latte -{foreach ($names|sort|reverse) as $name} - ... -{/foreach} -``` - -Jako parametr lze předat vlastní porovnávací funkci: .{data-version:2.10.2} - -```latte -{var $sorted = ($names|sort: fn($a, $b) => $b <=> $a)} -``` - - -spaceless .[filter]{data-version:2.10.2} ----------------------------------------- -Odstraní zbytečné bílé místo (mezery) z výstupu. Můžete také použít alias `strip`. - -```latte -{block |spaceless} -
      -
    • Hello
    • -
    -{/block} -``` - -Vypíše: - -```latte -
    • Hello
    -``` - - -stripHtml .[filter] -------------------- -Převádí HTML na čistý text. Tedy odstraní z něj HTML značky a HTML entity převede na text. - -```latte -{='

    one < two

    '|stripHtml} {* vypíše 'one < two' *} -``` - -Výsledný čistý text může přirozeně obsahovat znaky, které představují HTML značky, například `'<p>'|stripHtml` se převede na `

    `. V žádném případě nevypisujte takto vzniklý text s `|noescape`, protože to může vést ke vzniku bezpečnostní díry. - - -substr(int offset, int length = null) .[filter] ------------------------------------------------ -Extrahuje část řetězce. Tento filtr byl nahrazen filtrem [#slice]. - -```latte -{$string|substr: 1, 2} -``` - - -translate(string message, ...args) .[filter]{data-version:3.0} --------------------------------------------------------------- -Překládá výrazy do jiných jazyků. Aby byl filtr k dispozici, je potřeba [nastavit překladač|develop#TranslatorExtension]. Můžete také použít [tagy pro překlad|tags#Překlady]. - -```latte -{='Košík'|translate} -{$item|translate} -``` - - -trim(string charlist = " \t\n\r\0\x0B\u{A0}") .[filter] -------------------------------------------------------- -Odstraní prázdné znaky (nebo jiné znaky) od začátku a konce řetězce. - -```latte -{=' I like Latte. '|trim} {* vypíše 'I like Latte.' *} -{=' I like Latte.'|trim: '.'} {* vypíše ' I like Latte' *} -``` - - -truncate(int length, string append = '…') .[filter] ---------------------------------------------------- -Ořízne řetězec na uvedenou maximální délku, přičemž se snaží zachovávat celá slova. Pokud dojde ke zkrácení řetězce, přidá nakonec trojtečku (lze změnit druhým parametrem). - -```latte -{var $title = 'Hello, how are you?'} -{$title|truncate:5} {* Hell… *} -{$title|truncate:17} {* Hello, how are… *} -{$title|truncate:30} {* Hello, how are you? *} -``` - - -upper .[filter] ---------------- -Převede řetězec na velká písmena. Vyžaduje PHP rozšíření `mbstring`. - -```latte -{='latte'|upper} {* vypíše 'LATTE' *} -``` - -Viz také [#capitalize], [#firstUpper], [#lower]. - - -webalize .[filter] ------------------- -Upraví UTF‑8 řetězec do tvaru používaného v URL. - -Převádí se na ASCII. Převede mezery na pomlčky. Odstraní znaky, které nejsou alfanumerické, podtržítka ani pomlčky. Převede na malá písmena. Také odstraní přední a koncové mezery. - -```latte -{var $s = 'Náš 10. produkt'} -{$s|webalize} {* vypíše 'nas-10-produkt' *} -``` - -.[caution] -Vyžaduje knihovnu [nette/utils|utils:]. diff --git a/latte/cs/functions.texy b/latte/cs/functions.texy deleted file mode 100644 index 164e982d8b..0000000000 --- a/latte/cs/functions.texy +++ /dev/null @@ -1,126 +0,0 @@ -Latte funkce -************ - -.[perex] -V šablonách můžeme kromě běžných PHP funkcí používat i tyto další. - -.[table-latte-filters] -| `clamp` | [ohraničí hodnotu do daného rozsahu |#clamp] -| `divisibleBy`| [zkontroluje, zda je proměnná dělitelná číslem |#divisibleBy] -| `even` | [zkontroluje, zda je dané číslo sudé |#even] -| `first` | [vrací první prvek pole nebo znak řetězce |#first] -| `last` | [vrací poslední prvek pole nebo znak řetězce |#last] -| `odd` | [zkontroluje, zda je dané číslo liché |#odd] -| `slice` | [extrahuje část pole nebo řetězce |#slice] - - -Použití -======= - -Funkce se používají strejně jaké běžné PHP funkce a lze je použít ve všechn výrazech: - -```latte -

    {clamp($num, 1, 100)}

    - -{if odd($num)} ... {/if} -``` - -[Vlastní funkce|extending-latte#funkce] lze registrovat tímto způsobem: - -```php -$latte = new Latte\Engine; -$latte->addFunction('shortify', function (string $s, int $len = 10): string { - return mb_substr($s, 0, $len); -}); -``` - -V šabloně se potom volá takto: - -```latte -

    {shortify($text)}

    -

    {shortify($text, 100)}

    -``` - - -Funkce -====== - - -clamp(int|float $value, int|float $min, int|float $max): int|float .[method]{data-version:2.9} ----------------------------------------------------------------------------------------------- -Ohraničí hodnotu do daného inkluzivního rozsahu min a max. - -```latte -{=clamp($level, 0, 255)} -``` - -Viz také [filtr clamp|filters#clamp]. - - -divisibleBy(int $value, int $by): bool .[method]{data-version:2.10.2} ---------------------------------------------------------------------- -Zkontroluje, zda je proměnná dělitelná číslem. - -```latte -{if divisibleBy($num, 5)} ... {/if} -``` - - -even(int $value): bool .[method]{data-version:2.10.2} ------------------------------------------------------ -Zkontroluje, zda je dané číslo sudé. - -```latte -{if even($num)} ... {/if} -``` - - -first(string|array $value): mixed .[method]{data-version:2.10.2} ----------------------------------------------------------------- -Vrací první prvek pole nebo znak řetězce: - -```latte -{=first([1, 2, 3, 4])} {* vypíše 1 *} -{=first('abcd')} {* vypíše 'a' *} -``` - -Viz také [#last], [filtr first|filters#first]. - - -last(string|array $value): mixed .[method]{data-version:2.10.2} ---------------------------------------------------------------- -Vrací poslední prvek pole nebo znak řetězce: - -```latte -{=last([1, 2, 3, 4])} {* vypíše 4 *} -{=last('abcd')} {* vypíše 'd' *} -``` - -Viz také [#first], [filtr last|filters#last]. - - -odd(int $value): bool .[method]{data-version:2.10.2} ----------------------------------------------------- -Zkontroluje, zda je dané číslo liché. - -```latte -{if odd($num)} ... {/if} -``` - - -slice(string|array $value, int $start, int $length=null, bool $preserveKeys=false): string|array .[method]{data-version:2.10.2} -------------------------------------------------------------------------------------------------------------------------------- -Extrahuje část pole nebo řetězce. - -```latte -{=slice('hello', 1, 2)} {* vypíše 'el' *} -{=slice(['a', 'b', 'c'], 1, 2)} {* vypíše ['b', 'c'] *} -``` - -Filtr funguje jako funkce PHP `array_slice` pro pole nebo `mb_substr` pro řetězce s fallbackem na funkci `iconv_substr` v režimu UTF‑8. - -Pokud je start kladný, posloupnost začné posunutá o tento počet od začátku pole/řetezce. Pokud je záporný posloupnost začné posunutá o tolik od konce. - -Pokud je zadaný parametr length a je kladný, posloupnost bude obsahovat tolik prvků. Pokud je do této funkce předán záporný parametr length, posloupnost bude obsahovat všechny prvky původního pole, začínající na pozici start a končicí na pozici menší na length prvků od konce pole. Pokud tento parametr nezadáte, posloupnost bude obsahovat všechny prvky původního pole, začínající pozici start. - -Ve výchozím nastavení filtr změní pořadí a resetuje celočíselného klíče pole. Toto chování lze změnit nastavením preserveKeys na true. Řetězcové klíče jsou vždy zachovány, bez ohledu na tento parametr. diff --git a/latte/cs/guide.texy b/latte/cs/guide.texy deleted file mode 100644 index 642a4dd067..0000000000 --- a/latte/cs/guide.texy +++ /dev/null @@ -1,42 +0,0 @@ -Začínáme s Latte -**************** - -
    - -Latte je mimořádný šablonovací systém. Zamilujete si jeho syntaxi. A zároveň jde o jediný šablonovací systém pro PHP s [opravdu efektivní ochranou|safety-first] proti kritickým zranitelnostem. - - -Jak psát šablony v Latte? -------------------------- - -Jazky Latte je důmyslně navržený. Naučíte se ho rychle. Vystačíte si totiž se znalostí PHP a několika málo značek. - -- Nejprve se seznamte se [syntaxí Latte|syntax] a [zkoušejte vše online |https://fiddle.nette.org/latte/] -- Prohlédněte si [základní sadu značek |tags] a [filtrů |filters] -- Pište šablony v [editoru s podporu Latte |recipes#Editory a IDE] - - -Jak použít Latte v PHP? ------------------------ - -Nasadit Latte do vaší nové aplikace je otázkou pár minut: - -- Nejprve [Latte nainstalujte a spusťte |develop#Instalace] -- Nechte se rozmazlovat [debuggovacím nástrojem Tracy |develop#Debuggování a Tracy] -- Rozšiřte Latte o [vlastní funkcionalitu |extending-latte] - - -Co ještě Latte umí? -------------------- - -Latte dostáváte v plné výbavě, se vším důležitým v základu. - -- Vaši produktivitu naboostují [mechanismy dědičnosti |template-inheritance] díky kterým se opakované prvky a struktury znovupoužijí -- Pancéřový bunkr [sandbox] izoluje šablony z nedůvěryhodných zdrojů, které například editují samotní uživatelé -- Pro další inspiraci jsou tu [tipy a triky |recipes] - -
    - - - -{{description: Latte je nejbezpečnější šablonovací systém pro PHP. Zabraňuje spoustě bezpečnostních zranitelností. Oceníte jeho intuitivní syntaxi a oceníte spoustu užitečných vychytávek.}} diff --git a/latte/cs/recipes.texy b/latte/cs/recipes.texy deleted file mode 100644 index d9489358ca..0000000000 --- a/latte/cs/recipes.texy +++ /dev/null @@ -1,163 +0,0 @@ -Tipy a triky -************ - - -Editory a IDE -============= - -Pište šablony v editoru nebo IDE, který má podporu pro Latte. Bude to mnohem příjemnější. - -- NetBeans IDE má podporu vestavěnou -- PhpStorm: nainstalujte v `Settings > Plugins > Marketplace` [plugin Latte|https://plugins.jetbrains.com/plugin/7457-latte] -- VS Code: hledejte v markerplace "Nette Latte + Neon" plugin -- Sublime Text 3: v Package Control najděte a nainstalujte balíček `Nette` a zvolte Latte ve `View > Syntax` -- ve starých editorech použijte pro soubory .latte zvýrazňování Smarty - -Plugin pro PhpStorm je velmi pokročilý a umí výborně napovídat PHP kód. Aby fungoval optimálně, používejte [typované šablony|type-system]. - -[* latte-phpstorm-plugin.webp *] - -Podporu pro Latte najdete také ve webovém zvýrazňovači kódu [Prism.js|https://prismjs.com/#supported-languages] a editoru [Ace|https://ace.c9.io]. - - -Latte uvnitř JavaScriptu nebo CSS -================================= - -Latte lze velmi pohodlně používat i uvnitř JavaScriptu nebo CSS. Jak se však vyhnout situaci, kdy by Latte mylně považovalo JavaScriptový kód nebo CSS styl za Latte značku? - -```latte - - - -``` - -**Varianta 1** - -Vyhněte se situaci, kdy následuje písmeno hned za `{`, třeba tím, že před něj vložíte mezeru, odřádkování nebo uvozovku: - -```latte - - - -``` - -**Varianta 2** - -Zcela vypněte zpracování Latte značek uvnitř elementu pomocí [n:syntax |tags#syntax]: - -```latte - -``` - -**Varianta 3** - -Přepněte uvnitř elementu syntax Latte značek na zdvojené složené závorky: - -```latte - -``` - -V JavaScriptu [se nepíší uvozovky kolem proměnné |tags#Vypsání v JavaScriptu]. - - -Náhrada `use` klausule v Latte -============================== - -Jak v Latte nahradit klauzule `use`, které se používají v PHP, abyste nemuseli psát namespace při přistupování k třídě? Příklad v PHP: - -```php -use Pets\Model\Dog; - -if ($dog->status === Dog::STATUS_HUNGRY) { - // ... -} -``` - -**Varianta 1** - -Místo klausule `use` si uložíme název třídy do proměnné a následně místo `Dog` používáme `$Dog`: - -```latte -{var $Dog = Pets\Model\Dog::class} - -
    - {if $dog->status === $Dog::STATUS_HUNGRY} - ... - {/if} -
    -``` - -**Varianta 2** - -Pokud je objekt `$dog` instancí `Pets\Model\Dog`, pak lze použít `{if $dog->status === $dog::STATUS_HUNGRY}`. - - -Generování XML v Latte -====================== - -Latte může generovat jakýkoli textový formát (HTML, XML, CSV, iCal atd.), nicméně aby správě escapovalo vypisované data, musíme mu říct, jaký formát generujeme. K tomu slouží značka [`{contentType}`|tags#contentType]. - -```latte -{contentType application/xml} - -... -``` - -Poté můžeme například vygenerovat sitemapu podobným způsobem: - -```latte -{contentType application/xml} - - - - {$url->loc} - {$url->lastmod->format('Y-m-d')} - {$url->frequency} - {$url->priority} - - -``` - - -Předání dat z includované šablony -================================= - -Proměnné, které vytvoříme pomocí `{var}` či `{default}` v inkludované šabloně, existují jen v ní a nejsou v inkludující šabloně k dispozici. -Pokud bychom si chtěli z inkludované šablony předat zpátky do inkludující nějaká data, jednou z možností je předat do šablony objekt a do něj data vložit. - -Hlavní šablona: - -```latte -{* vytvoří prázdný objekt $vars *} -{var $vars = (object) null} - -{include 'included.latte', vars: $vars} - -{* nyní obsahuje property foo *} -{$vars->foo} -``` - -Inkludovaná šablona `included.latte`: - -```latte -{* zapíšeme data do property foo *} -{var $vars->foo = 123} -``` diff --git a/latte/cs/safety-first.texy b/latte/cs/safety-first.texy deleted file mode 100644 index 44bd9f4edf..0000000000 --- a/latte/cs/safety-first.texy +++ /dev/null @@ -1,371 +0,0 @@ -Latte je synonymum bezpečnosti -****************************** - -
    - -Latte je jediný šablonovací systém pro PHP s efektivní ochranou proti kritické zranitelnosti Cross-site Scripting (XSS). A to díky tzv. kontextově sensitivnímu escapování. Povíme si, - -- jaký je princip zranitelnosti XSS a proč je tak nebezpečná -- čím to, že je Latte v obraně před XSS tak efektivní -- jak lze v šablonách Twig, Blade a spol. snadno udělat bezpečnostní díru - -
    - - -Cross-site Scripting (XSS) -========================== - -Cross-site Scripting (zkráceně XSS) je jednou z nejčastějších zranitelností webových stránek a přitom velmi nebezpečnou. Umožní útočníkovi vložit do cizí stránky škodlivý skript (tzv. malware), který se spustí v prohlížeči nic netušícího uživatele. - -Co všechno může takový skript napáchat? Může například odeslat útočníkovi libovolný obsah z napadené stránky, včetně citlivých údajů zobrazených po přihlášení. Může stránku pozměnit nebo provádět další požadavky jménem uživatele. -Pokud by se například jednalo o webmail, může si přečíst citlivé zprávy, pozměnit zobrazovaný obsah nebo přenastavit konfiguraci, např. zapnout přeposílání kopií všech zpráv na útočníkovu adresu, aby získal přístup i k budoucím emailům. - -Proto také XSS figuruje na předních místech žebříčků nejnebezpečnějších zranitelností. Pokud se na webové stránce zranitelnost objeví, je nutné ji co nejdříve odstranit, aby se zabránilo zneužití. - - -Jak zranitelnost vzniká? ------------------------- - -Chyba vzniká v místě, kde se webová stránka generuje a vypisují se proměnné. Představte si, že vytváříte stránku s vyhledáváním, a na začátku bude odstavec s hledaným výrazem v podobě: - -```php -echo '

    Výsledky vyhledávání pro ' . $search . '

    '; -``` - -Útočník může do vyhledávacího políčka a potažmo do proměnné `$search` zapsat libovolný řetězec, tedy i HTML kód jako ``. Protože výstup není nijak ošetřen, stane se součástí zobrazené stránky: - -```html -

    Výsledky vyhledávání pro

    -``` - -Prohlížeč místo toho, aby vypsal hledaný řetězec, spustí JavaScript. A tím přebírá vládu nad stránkou útočník. - -Můžete namítnout, že vložením kódu do proměnné sice dojde ke spuštění JavaScriptu, ale jen v útočníkově prohlížeči. Jak se dostane k oběti? Z tohoto pohledu rozlišujeme několik typů XSS. V našem příkladu s vyhledáváním hovoříme o *reflected XSS*. -Zde je ještě potřeba navést oběť, aby klikla na odkaz, který bude obsahovat škodlivý kód v parametru: - -``` -https://example.com/?search= -``` - -Navedení uživatele na odkaz sice vyžaduje určité sociální inženýrství, ale není to nic složitého. Uživatelé na odkazy, ať už v emailech nebo na sociálních sítích, klikají bez větších rozmyslů. A že je v adrese něco podezřelého se dá zamaskovat pomocí zkracovače URL, uživatel pak vidí jen `bit.ly/xxx`. - -Nicméně existuje i druhá a mnohem nebezpečnější forma útoku označovaná jako *stored XSS* nebo *persistent XSS*, kdy se útočníkovi podaří uložit škodlivý kód na server tak, aby byl automaticky vkládán do některých stránek. - -Příkladem jsou stránky, kam uživatelé píší komentáře. Útočník pošle příspěvek obsahující kód a ten se uloží na server. Pokud stránky nejsou dostatečně zabezpečené, bude se pak spouštět v prohlížeči každého návštěvníka. - -Mohlo by se zdát, že jádro útoku spočívá v tom dostat do stránky řetězec ` - - - -

    -``` - -Dvě cesty a dva různé způsoby escapování dat. Uvnitř elementu ` -``` - -Pokud bychom jej však chtěli vložit do HTML atributu, musíme ještě escapovat uvozovky na HTML entity: - -```html -
    -``` - -Vnořeným kontextem ale nemusí být jen JS nebo CSS. Běžně jím je také URL. Parametry v URL se escapují tak, že se znaky se speciálním významen převádějí na sekvence začínající `%`. Příklad: - -``` -https://example.org/?a=Jazz&b=Rock%27n%27Roll -``` - -A když tento řetězec vypíšeme v atributu, ještě aplikujeme escapování podle tohoto kontextu a nahradíme `&` za `&`: - -```html - -``` - -Pokud jste dočetli až sem, gratulujeme, bylo to vyčerpávající. Teď už máte dobrou představu o tom, co jsou to kontexty a escapování. A nemusíte mít obavy, že je to složité. Latte tohle totiž dělá za vás automaticky. - - -Latte vs naivní systémy -======================= - -Ukázali jsem si, jak se správně escapuje v HTML dokumentu a jak zásadní je znalost kontextu, tedy místa, kde data vypisujeme. Jinými slovy, jak funguje kontextově sensitvní escapování. -Ačkoliv jde o nezbytný předpoklad funkční obrany před XSS, **Latte je jediný šablonovací systém pro PHP, který tohle umí.** - -Jak je to možné, když všechny systémy dnes tvrdí, že mají automatické escapování? -Automatické escapování bez znalosti kontextu je trošku bullshit, který **vytváří falešný dojem bezpečí**. - -Šablonovací systémy, jako je Twig, Laravel Blade a další, nevidí v šabloně žádnou HTML strukturu. Nevidí tudíž ani kontexty. Oproti Latte jsou slepé a naivní. Zpracovávají jen vlastní značky, vše ostatní je pro ně nepodstatný tok znaků: - -
    - -```twig .{file:Twig šablona, jak ji vidí samotný Twig} -░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░ -░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░ -``` - -```twig .{file:Twig šablona, jak ji vidí designer} -- v textu: {{ text }} -- v tagu: -- v atributu: -- v atributu bez uvozovek: -- v atributu obsahujícím URL: -- v atributu obsahujícím JavaScript: -- v atributu obsahujícím CSS: -- v JavaScriptu: -- v CSS: -- v komentáři: -``` - -
    - -Naivní systémy jen mechanicky převádějí znaky `< > & ' "` na HTML entity, což je sice ve většině případů užití platný způsob escapování, ale zdaleka ne vždy. Nemohou tak odhalit ani předejít vzniku různých bezpečnostní děr, jak si ukážeme dále. - -Latte šablonu vidí stejně jako vy. Chápe HTML, XML, rozeznává značky, atributy atd. A díky tomu rozlišuje jednotlivé kontexty a podle nich ošetřuje data. Nabízí tak opravdu efektivní ochranu proti kritické zranitelnosti Cross-site Scripting. - - -Živá ukázka -=========== - -Vlevo vidíte šablonu v Latte, vpravo je vygenerovaný HTML kód. Několikrát se tu vypisuje proměnná `$text` a pokaždé v trošku jiném kontextu. A tedy i trošku jinak escapovaná. Kód šablony můžete sami editovat, například změnit obsah proměnné atd. Zkuste si to: - -
    -
    - -``` .{file:template.latte; min-height: 14em}[fiddle-source] -{* ZKUS UPRAVIT TUTO ŠABLONU *} -{var $text = "Rock'n'Roll"} -- {$text} -- -- -- -- -- -``` - -
    - -
    - -``` .{file:view-source:...; min-height: 14em}[fiddle-output] -- Rock'n'Roll -- -- -- -- -- -``` - -
    -
    - -Není to skvělé! Latte dělá kontextově sensitivní escapování automaticky, takže programátor: - -- nemusí přemýšlet ani vědět, jak se kde escapuje -- nemůže se splést -- nemůže na escapování zapomenout - -Tohle dokonce nejsou všechny kontexty, které Latte při vypisování rozlišuje a pro které přizpůsobuje ošetření dat. Další zajimavé případy si projdeme nyní. - - -Jak hacknout naivní systémy -=========================== - -Na několika praktických příkladech si ukážeme, jak je rozlišování kontextů důležité a proč naivní šablonovací systémy neposkytují dostatečnou ochranu před XSS, na rozdíl od Latte. -Jako zástupce naivního systému použijeme v ukázkách Twig, ale totéž platí i pro ostatní systémy. - - -Zranitelnost atributem ----------------------- - -Pokusíme se do stránky injektovat škodlivý kód pomocí HTML atributu, jak jsme si [ukazovali výše|#Jak zranitelnost vzniká]. Mějme šablonu v Twigu vykreslující obrázek: - -```twig .{file:Twig} -{{ -``` - -Všimněte si, že okolo hodnot atributů nejsou uvozovky. Kodér na ně mohl zapomenout, což se prostě stává. Například v Reactu se kód píše takto, bez uvozovek, a kodér, který střídá jazyky, pak na uvozovky může snadno zapomenout. - -Útočník jako popisek obrázku vloží šikovně sestavený řetězec `foo onload=alert('Hacked!')`. Už víme, že Twig nemůže poznat, jestli se proměnná vypisuje v toku HTML textu, uvnitř atributu, HTML komentáře, atd., zkrátka nerozlišuje kontexty. A jen mechanicky převádí znaky `< > & ' "` na HTML entity. -Takže výsledný kód bude vypadat takto: - -```html -foo -``` - -**A vznikla bezpečností díra!** - -Součástí stránky se stal podvržený atribut `onload` a prohlížeč ihned po stažení obrázku jej spustí. - -Nyní se podíváme, jak si se stejnou šablonou poradí Latte: - -```latte .{file:Latte} -{$imageAlt} -``` - -Latte vidí šablonu stejně jako vy. Na rozdíl od Twigu chápe HTML a ví, že proměnná se vypisuje jako hodnota atributu, který není v uvozovkách. Proto je doplní. Když útočník vloží stejný popisek, výsledný kód bude vypadat takto: - -```html -foo onload=alert('Hacked!') -``` - -**Latte úspěšně zabránilo XSS.** - - -Vypsání proměnné v JavaScript ------------------------------ - -Díky kontextově sensitivnímu escapování je možné zcela nativně používat PHP proměnné uvnitř JavaScriptu. - -```latte -

    {$movie}

    - - -``` - -Pokud bude proměnná `$movie` obsahovat řetězec `'Amarcord & 8 1/2'`, vygeneruje se následující výstup. Všimněte si, že uvnitř HTML se použije jiné escapování, než uvnitř JavaScriptu a ještě jiné v atributu `onclick`: - -```latte -

    Amarcord & 8 1/2

    - - -``` - - -Kontrola odkazů ---------------- - -Latte automaticky kontroluje, zda proměnná použitá v atributech `src` nebo `href` obsahuje webovou URL (tj. protokol HTTP) a předchází vypsání odkazů, které mohou představovat bezpečnostní riziko. - -```latte -{var $link = 'javascript:attack()'} - -klikni -``` - -Vypíše: - -```latte -klikni -``` - -Kontrola se dá vypnout pomocí filtru [nocheck|filters#nocheck]. - - -Limity Latte -============ - -Latte není zcela kompletní ochranou před XSS pro celou aplikaci. Byli bychom neradi, kdybyste při použití Latte přestali nad bezpečností přemýšlet. -Cílem Latte je zajistit, aby útočník nemohl pozměnit strukturu stránky, podvrhnout HTML elementy nebo atributy. Ale nekontroluje obsahovou správnost vypisovaných dat. Nebo správnost chování JavaScriptu. -To už jde mimo kompetence šablonovacího systému. Ověřování správnosti dat, zejména těch vložených uživatelem a tedy nedůvěryhodných, je důležitým úkolem programátora. diff --git a/latte/cs/sandbox.texy b/latte/cs/sandbox.texy deleted file mode 100644 index efce7b0dec..0000000000 --- a/latte/cs/sandbox.texy +++ /dev/null @@ -1,54 +0,0 @@ -Sandbox -******* - -.[perex]{data-version:2.8} -Latte má pancéřový bunkr přímo pod kapotou. Říká se mu sandbox režim a jde o důležitou funkci chránící aplikace, ve kterých se používají šablony z nedůvěryhodných zdrojů. Například když je editují samotní uživatelé. - -Sandbox znamená pískoviště a tento režim hlídá, aby se písek nedostal mimo vyhrazenou plochu. Tedy poskytuje omezený přístup k makrům, filtrům, funkcím, metodám atd. -Jak to funguje? Jednoduše nadefinujeme, co všechno šabloně dovolíme. Přičemž v základu je všechno zakázané a my postupně povolujeme: - -Následujícím kódem umožníme autorovi šablony používat značky `{block}`, `{if}`, `{else}` a `{=}`, což je značka pro [vypsání proměnné nebo výrazu|tags#Vypisování] a všechny filtry: - -```php -$policy = new Latte\Sandbox\SecurityPolicy; -$policy->allowTags(['block', 'if', 'else', '=']); -$policy->allowFilters($policy::ALL); - -$latte->setPolicy($policy); -``` - -Dále můžeme povolit jednotlivé funkce, metody nebo properties objektů: - -```php -$policy->allowFunctions(['trim', 'strlen']); -$policy->allowMethods(Nette\Security\User::class, ['isLoggedIn', 'isAllowed']); -$policy->allowProperties(Nette\Database\Row::class, $policy::ALL); -``` - -Není to úžasné? Můžete na velmi nízké úrovni kontrolovat úplně všechno. Pokud se šablona pokusí zavolat nepovolenou funkci nebo přistoupit k nepovolené metodě nebo property, skončí to výjimkou `Latte\SecurityViolationException`. - -Tvořit policy od bodu nula, kdy je zakázáno úplně vše, nemusí být pohodlné, proto můžete začít od bezpečného základu: - -```php -$policy = Latte\Sandbox\SecurityPolicy::createSafePolicy(); -``` - -Bezpečný základ znamená, že jsou povoleny všechny standardní tagy kromě `contentType`, `debugbreak`, `dump`, `extends`, `import`, `include`, `layout`, `php`, `sandbox`, `snippet`, `snippetArea`, `templatePrint`, `varPrint`, `widget`. -Jsou povoleny standardní filtry kromě `datastream`, `noescape` a `nocheck`. A nakonec je povolený přístup k metodám a properites objektu `$iterator`. - -Pravidla se aplikují pro šablonu, kterou vložíme značkou [`{sandbox}` |tags#Vložení šablon]. Což je jakási obdoba `{include}`, která však zapíná bezpečný režim a také nepředává žádné proměnné: - -```latte -{sandbox 'untrusted.latte'} -``` - -Tedy layout a jednotlivé stránky mohou nerušeně využívat všechny tagy a proměnné, pouze na šablonu `untrusted.latte` budou uplatněny restrikce. - -Některé prohřešky, jako použití zakázaného tagu nebo filtru, se odhalí v době kompilace. Jiné, jako třeba volání nepovolených metod objektu, až za běhu. -Šablona také může obsahovat jakékoliv jiné chyby. Aby vám ze sandboxované šablony nemohla vyskočit výjimka, která naruší celé vykreslování, lze definovat vlastní [obslužný handler pro výjimky|develop#exception handler], který ji třeba zaloguje. - -Pokud bychom chtěli sandbox režim zapnout přímo pro všechny šablony, jde to snadno: - -```php -$latte->setSandboxMode(); -``` diff --git a/latte/cs/syntax.texy b/latte/cs/syntax.texy deleted file mode 100644 index d9d5c331a6..0000000000 --- a/latte/cs/syntax.texy +++ /dev/null @@ -1,272 +0,0 @@ -Syntaxe -******* - -.[perex] -Syntax Latte vzešla z praktických požadavků webdesignerů. Hledali jsme tu nejpřívětivější syntax, se kterou elegantně zapíšete i konstrukce, které jinak představují skutečný oříšek. -Zároveň všechny výrazy se píší úplně stejně jako v PHP, takže se nemusíte učit nový jazyk. Prostě zúročíte co už dávno umíte. - -Níže je uvedena minimální šablona, která ilustruje několik základních prvků: tagy, n:atributy, komentáře a filtry. - -```latte -{* toto je komentář *} -
      {* n:if je n:atribut *} -{foreach $items as $item} {* tag představující cyklus foreach *} -
    • {$item|capitalize}
    • {* tag vypisující proměnnou s filtrem *} -{/foreach} {* konec cyklu *} -
    -``` - -Podívejme se blíže na tyto důležité prvky a na to, jak vám mohou pomoci vytvořit úžasnou šablonu. - - -Tagy -==== - -Šablona obsahuje tagy (neboli značky či makra), které řídí logiku šablony (například smyčky *foreach*) nebo vypisují výrazy. Pro obojí se používá jediný delimiter `{ ... }`, takže nemusíte přemýšlet, jaký delimiter v jaké situaci použít, jako je tomu u jiných systémů. -Pokud za znakem `{` následuje uvozovka nebo mezera, Latte jej nepovažuje za začátek značky, díky čemuž můžete v šablonách bez problémů používat i JavaScriptové konstrukce, JSON nebo pravidla v CSS. - -Podívejte se na [přehled všech tagů|tags]. Krom toho si můžete vytvářet i [vlastní tagy|extending-latte#tagy]. - - -Latte rozumí PHP -================ - -Uvnitř značek můžete používat PHP výrazy, které dobře znáte: - -- proměnné -- řetězce (včetně HEREDOC a NOWDOC), pole, čísla, apod. -- [operátory |https://www.php.net/manual/en/language.operators.php] -- volání funkcí a metod (které lze omezit [sandboxem|sandbox]) -- [match |https://www.php.net/manual/en/control-structures.match.php] -- [anonymní funkce |https://www.php.net/manual/en/functions.arrow.php] -- [callbacky |https://www.php.net/manual/en/functions.first_class_callable_syntax.php] -- víceřádkové komentáře `/* ... */` -- atd… - -Latte navíc syntaxi PHP doplňuje o několik [příjemných rozšíření |#Syntaktický cukr]. - - -n:atributy -========== - -Všechny párové značky, například `{if} … {/if}`, operující nad jedním HTML elementem, se dají přepsat do podoby n:atributů. Takto by bylo možné zapsat například i `{foreach}` v úvodní ukázce: - -```latte -
      -
    • {$item|capitalize}
    • -
    -``` - -Funkcionalita se pak vztahuje na HTML element, do něhož je umístěný: - -```latte -{var $items = ['I', '♥', 'Latte']} - -

    {$item}

    -``` - -vypíše: - -```latte -

    I

    -

    -

    Latte

    -``` - -Pomocí prefixu `inner-` můžeme chování poupravit tak, aby se vztahovalo jen na vnitřní část elementu: - -```latte -
    -

    {$item}

    -
    -
    -``` - -Vypíše se: - -```latte -
    -

    I

    -
    -

    -
    -

    Latte

    -
    -
    -``` - -Nebo pomocí prefixu `tag-` aplikujeme funkcionalitu jen na samotné HTML značky: - -```latte -

    Title

    -``` - -Což vypíše v závislosti na proměnné `$url`: - -```latte -{* když je $url prázdné *} -

    Title

    - -{* když $url obsahuje 'https://nette.org' *} -

    Title

    -``` - -Avšak n:atributy nejsou jen zkratkou pro párové značky. Existují i ryzí n:atributy, jako třeba [n:href |application:creating-links#V šabloně presenteru] nebo velešikovný pomocník kodéra [n:class |tags#n:class]. - - -Filtry -====== - -Podívejte se na přehled [standardních filtrů |filters]. - -Filtry se zapisují za svislítko (může být před ním mezera): - -```latte -

    {$heading|upper}

    -``` - -Filtry lze zřetězit a poté se aplikují v pořadí od levého k pravému: - -```latte -

    {$heading|lower|capitalize}

    -``` - -Parametry se zadávají za jménem filtru oddělené dvojtečkami nebo čárkami: - -```latte -

    {$heading|truncate:20,''}

    -``` - -Filtry lze aplikovat i na výraz: - -```latte -{var $name = ($title|upper) . ($subtitle|lower)} -``` - -Na blok: - -```latte -

    {block |lower}{$heading}{/block}

    -``` - -Nebo přímo na hodnotu (v kombinaci s tagem [`{=expr}`| https://latte.nette.org/cs/tags#Vypisování]): -```latte -

    {=' Hello world '|trim}

    -``` - - -Komentáře -========= - -Komentáře se zapisují tímto způsobem a do výstupu se nedostanou: - -```latte -{* tohle je komentář v Latte *} -``` - -Uvnitř značek fungují PHP komentáře: - -```latte -{include 'file.info', /* value: 123 */} -``` - - -Syntaktický cukr -================ - - -Řetězce bez uvozovek --------------------- - -U jednoduchých řetězců lze vynechat uvozovky: - -```latte -jako v PHP: {var $arr = ['hello', 'btn--default', '€']} - -zkráceně: {var $arr = [hello, btn--default, €]} -``` - -Jednoduché řetězce jsou ty, které jsou tvořeny čistě z písmen, číslic, podtržítek a pomlček. Nesmí začínat číslicí a nesmí začínat nebo končit pomlčkou. -Nesmí být složený jen z velkých písmen a podtržítek, protože pak se považuje za konstantu (např. `PHP_VERSION`). -A nesmí kolidovat s klíčovými slovy: `and`, `array`, `clone`, `default`, `false`, `in`, `instanceof`, `new`, `null`, `or`, `return`, `true`, `xor`. - - -Zkrácený ternární operátor --------------------------- - -Je-li třetí hodnota ternárního operátoru prázdná, lze ji vynechat: - -```latte -jako v PHP: {$stock ? 'Skladem' : ''} - -zkráceně: {$stock ? 'Skladem'} -``` - - -Moderní zápis klíčů v poli --------------------------- - -Klíče v poli lze zapisovat podobně jako pojmenované parametry při volání funkcí: - -```latte -jako v PHP: {var $arr = ['one' => 'item 1', 'two' => 'item 2']} - -moderně: {var $arr = [one: 'item 1', two: 'item 2']} -``` - - -Filtry ------- - -Filtry lze použít pro jakékoliv výrazy, stačí celek uzavřít do závorek: - -```latte -{var $content = ($text|truncate: 30|upper)} -``` - - -Operátor `in` -------------- - -Operátorem `in` lze nahradit funkci `in_array()`. Porovnání je vždy striktní: - -```latte -{* obdoba in_array($item, $items, true) *} -{if $item in $items} - ... -{/if} -``` - - -.{data-version:2.9} -Volitelné řetězení s undefined-safe operátorem ----------------------------------------------- - -Undefined-safe operator `??->` je obdoba nullsafe operatoru `?->`, avšak nevyvolá chybu, pokud proměnná, property nebo index v poli vůbec neexistuje. - -```latte -{$order??->id} -``` - -říkáme tím, že když existuje `$order` a není null, bude vypsán `$order->id`, ale když je `$order` null nebo neexistuje, zastaví se vyhodnocování a prostě se vrátí null. - -```latte -{$user??->address??->street} -// znamená cca isset($user) && isset($user->address) ? $user->address->street : null -``` - - -Historické okénko ------------------ - -Latte přišlo v průběhu své historie s celou řadou syntaktických cukříků, které se po pár letech objevily v samotném PHP. Například v Latte bylo možné psát pole jako `[1, 2, 3]` místo `array(1, 2, 3)` nebo používat nullsafe operátor `$obj?->foo` dávno předtím, než to bylo možné v samotném PHP. Latte také zavedlo operátor pro rozbalení pole `(expand) $arr`, který je ekvivalentem dnešního operátoru `...$arr` z PHP. - - -Omezení PHP v Latte -=================== - -V Latte lze zapisovat jen PHP výrazy. Tedy nelze deklarovat třídy nebo používat [řídící struktury |https://www.php.net/manual/en/language.control-structures.php], např. `if`, `foreach`, `switch`, `return`, `try`, `throw` a další, místo kterých Latte nabízí své [značky|tags]. -Také nelze používat [atributy |https://www.php.net/manual/en/language.attributes.php], [backticks |https://www.php.net/manual/en/language.operators.execution.php] či [magické konstanty |https://www.php.net/manual/en/language.constants.magic.php], protože by to nedávalo smysl. -Nelze používat ani `unset`, `echo`, `include`, `require`, `exit`, `eval`, protože nejde o funkce, ale speciální jazykové konstrukce PHP, a nejsou to tedy výrazy. - -Tyto omezení lze nicméně obejít tím, že si aktivujete rozšíření [RawPhpExtension |develop#RawPhpExtension], díky kterému lze pak používat ve značce `{php ...}` jakýkoliv PHP kód na zodpovědnost autora šablony. diff --git a/latte/cs/tags.texy b/latte/cs/tags.texy deleted file mode 100644 index 27493b3d9a..0000000000 --- a/latte/cs/tags.texy +++ /dev/null @@ -1,1054 +0,0 @@ -Latte tagy (makra) -****************** - -.[perex] -Přehled a popis všech tagů (neboli značek či maker) šablonovacího systému Latte, které jsou vám standardně k dispozici. - -.[table-latte-tags language-latte] -|## Vypisování -| `{$var}`, `{...}` nebo `{=...}` | [vypíše escapovanou proměnnou nebo výraz |#Vypisování] -| `{$var\|filter}` | [vypíše s použitím filtrů |#Filtry] -| `{l}` nebo `{r}` | vypíše znak `{` nebo `}` - -.[table-latte-tags language-latte] -|## Podmínky -| `{if}` … `{elseif}` … `{else}` … `{/if}` | [podmínka if|#if-elseif-else] -| `{ifset}` … `{elseifset}` … `{/ifset}` | [podmínka ifset|#ifset-elseifset] -| `{ifchanged}` … `{/ifchanged}` | [test jestli došlo ke změně |#ifchanged] -| `{switch}` `{case}` `{default}` `{/switch}` | [podmínka switch|#switch-case-default] - -.[table-latte-tags language-latte] -|## Cykly -| `{foreach}` … `{/foreach}` | [#foreach] -| `{for}` … `{/for}` | [#for] -| `{while}` … `{/while}` | [#while] -| `{continueIf $cond}` | [pokračovat další iterací |#continueif-skipif-breakif] -| `{skipIf $cond}` | [přeskočit iteraci |#continueif-skipif-breakif] -| `{breakIf $cond}` | [přerušení cyklu |#continueif-skipif-breakif] -| `{exitIf $cond}` | [včasné ukončení |#exitif] -| `{first}` … `{/first}` | [jde o první průchod? |#first-last-sep] -| `{last}` … `{/last}` | [jde o poslední průchod? |#first-last-sep] -| `{sep}` … `{/sep}` | [bude ještě následovat průchod? |#first-last-sep] -| `{iterateWhile}` … `{/iterateWhile}` | [strukturovaný foreach|#iterateWhile] -| `$iterator` | [speciální proměnná uvnitř foreach |#$iterator] - -.[table-latte-tags language-latte] -|## Vložení dalších šablon -| `{include 'file.latte'}` | [načte šablonu z dalšího souboru |#include] -| `{sandbox 'file.latte'}` | [načte šablonu v sandbox režimu |#sandbox] - -.[table-latte-tags language-latte] -|## Bloky, layouty, dědičnost šablon -| `{block}` | [anonymní blok|#block] -| `{block blockname}` | [definuje blok |template-inheritance#bloky] -| `{define blockname}` | [definuje blok pro pozdější použití |template-inheritance#definice] -| `{include blockname}` | [vykreslení bloku|template-inheritance#Vykreslení bloků] -| `{include blockname from 'file.latte'}` | [vykreslí blok ze souboru |template-inheritance#Vykreslení bloků] -| `{import 'file.latte'}` | [načte bloky ze šablony |template-inheritance#horizontální znovupoužití] -| `{layout 'file.latte'}` / `{extends}` | [určuje soubor s layoutem |template-inheritance#Layoutová dědičnost] -| `{embed}` … `{/embed}` | [načte šablonu či blok a umožní přepsat bloky |template-inheritance#jednotková dědičnost] -| `{ifset blockname}` … `{/ifset}` | [podmínka, zda existuje blok |template-inheritance#Kontrola existence bloků] - -.[table-latte-tags language-latte] -|## Řízení výjimek -| `{try}` … `{else}` … `{/try}` | [zachycení výjimek |#try] -| `{rollback}` | [zahození bloku try |#rollback] - -.[table-latte-tags language-latte] -|## Proměnné -| `{var $foo = value}` | [vytvoří proměnnou |#var-default] -| `{default $foo = value}` | [vytvoří proměnnou, pokud neexistuje |#var-default] -| `{parameters}` | [deklaruje proměnné, typy a výchozí hodnoty |#parameters] -| `{capture}` … `{/capture}` | [zachytí blok do proměnné |#capture] - -.[table-latte-tags language-latte] -|## Typy -| `{varType}` | [deklaruje typ proměnné |type-system#varType] -| `{varPrint}` | [navrhne typy proměnných |type-system#varPrint] -| `{templateType}` | [deklaruje typy proměnných podle třídy |type-system#templateType] -| `{templatePrint}` | [navrhne třídu s typy proměnných |type-system#templatePrint] - -.[table-latte-tags language-latte] -|## Překlady -| `{_...}` | [vypíše překlad |#překlady] -| `{translate}` … `{/translate}` | [přeloží obsah |#překlady] - -.[table-latte-tags language-latte] -|## Ostatní -| `{contentType}` | [přepne escapování a pošle HTTP hlavičku |#contenttype] -| `{debugbreak}` | [umístí do kódu breakpoint |#debugbreak] -| `{do}` | [vykoná kód, ale nic nevypíše |#do] -| `{dump}` | [dumpuje proměnné do Tracy Bar |#dump] -| `{spaceless}` … `{/spaceless}` | [odstraní nadbytečné mezery |#spaceless] -| `{syntax}` | [změna syntaxe za běhu |#syntax] -| `{trace}` | [zobrazí stack trace |#trace] - -.[table-latte-tags language-latte] -|## Pomocníci HTML kodéra -| `n:class` | [dynamický zápis HTML atributu class |#n:class] -| `n:attr` | [dynamický zápis jakýchkoliv HTML atributů |#n:attr] -| `n:tag` | [dynamický zápis jména HTML elementu |#n:tag] -| `n:ifcontent` | [Vynechá prázdný HTML tag |#n:ifcontent] - -.[table-latte-tags language-latte] -|## Dostupné pouze v Nette Frameworku -| `n:href` | [odkaz používaný v HTML elementech `` |application:creating-links#V šabloně presenteru] -| `{link}` | [vypíše odkaz |application:creating-links#V šabloně presenteru] -| `{plink}` | [vypíše odkaz na presenter |application:creating-links#V šabloně presenteru] -| `{control}` | [vykreslí komponentu |application:components#Vykreslení] -| `{snippet}` … `{/snippet}` | [výstřižek, který lze odeslat AJAXem |application:ajax#tag-snippet] -| `{snippetArea}` | obálka pro výstřižky -| `{cache}` … `{/cache}` | [cachuje část šablony |caching:#cachovani-v-latte] - -.[table-latte-tags language-latte] -|## Dostupné pouze s Nette Forms -| `{form}` … `{/form}` | [vykreslí značky formuláře |forms:rendering#latte] -| `{label}` … `{/label}` | [vykreslí popisku formulářového prvku |forms:rendering#label-input] -| `{input}` | [vykreslí formulářový prvek |forms:rendering#label-input] -| `{inputError}` | [vypíše chybovou hlášku formulářového prvku |forms:rendering#inputError] -| `n:name` | [oživí formulářový prvek |forms:rendering#n:name] -| `{formPrint}` | [navrhne Latte kód pro formulář |forms:rendering#formPrint] -| `{formPrintClass}` | [navrhne PHP kód třídy s daty formuláře |forms:in-presenter#mapovani-na-tridy] -| `{formContext}` … `{/formContext}` | [částečné kreslení formuláře |forms:rendering#specialni-pripady] - - -Vypisování -========== - - -`{$var}` `{...}` `{=...}` -------------------------- - -V Latte se používá značka `{=...}` pro výpis jakéhokoliv výrazu na výstup. Latte záleží na vašem pohodlí, takže pokud výraz začíná proměnnou nebo voláním funkce, není potřeba rovnítko psát. Což v praxi znamená, že ho není potřeba psát téměř nikdy: - -```latte -Jméno: {$name} {$surname}
    -Věk: {date('Y') - $birth}
    -``` - -Jako výraz můžete zapsat cokoliv, co znáte z PHP. Nemusíte se zkrátka učit nový jazyk. Tak třeba: - - -```latte -{='0' . ($num ?? $num * 3) . ', ' . PHP_VERSION} -``` - -Prosím, nehledejte v předchozím příkladu žádný smysl, ale kdybyste tam nějaký našli, napište nám :-) - - -Escapování výstupu ------------------- - -Jaký je nejdůležitější úkol šablonovacího systému? Zamezit bezpečnostním dírám. A přesně tohle dělá Latte vždy, když něco vypisujete. Automaticky to escapuje: - -```latte -

    {='one < two'}

    {* vypíše: '

    one < two

    ' *} -``` - -Abychom byli přesní, Latte používá kontextově sensitivně escapování, což je tak důležitá a unikátní věc, že jsme tomu věnovali [samostatnou kapitolu|safety-first#Kontextově sensitivní escapování]. - -A co když vypisujte obsah kódovaný v HTML z důvěryhodného zdroje? Pak lze snadno escapování vypnout: - -```latte -{$trustedHtmlString|noescape} -``` - -.[warning] -Špatné použití filtru `noescape` může vést ke vzniku zranitelnosti XSS! Nikdy jej nepoužívejte, pokud si nejste **zcela jisti** co děláte, a že vypisovaný řetězec pochází z důvěryhodného zdroje. - - -Vypsání v JavaScriptu ---------------------- - -Díky kontextově sensitivnímu escapování je nádherně snadné vypisovat proměnné uvnitř JavaScriptu a správné escapování zařídí Latte. - -Proměnná nemusí být jen řetězec, podporován je kterýkoliv datový typ, který se pak zakóduje jako JSON: - -```latte -{var $foo = ['hello', true, 1]} - -``` - -Vygeneruje: - -```latte - -``` - -To je také důvod, proč se kolem proměnné **nepíší uvozovky**: Latte je u řetězců doplní samo. A pokud byste chtěli řetězcovou proměnnou vložit do jiného řetězce, jednoduše je spojte: - -```latte - -``` - - -Filtry ------- - -Vypsaný výraz může být upraven [filtrem|syntax#filtry]. Takto třeba řetězec převedeme na velká písmena a zkrátíme na maximálně 30 znaků: - -```latte -{$string|upper|truncate:30} -``` - -Filtry můžete používat i na dílčí části výrazu tímto způsobem: - -```latte -{$left . ($middle|upper) . $right} -``` - - -Podmínky -======== - - -`{if}` `{elseif}` `{else}` --------------------------- - -Podmínky se chovají stejně, jako jejich protějšky v PHP. Můžete v nich používat i stejné výrazy, jaké znáte z PHP, nemusíte se učit nový jazyk. - -```latte -{if $product->inStock > Stock::MINIMUM} - Skladem -{elseif $product->isOnWay()} - Na cestě -{else} - Není dostupné -{/if} -``` - -Jako každou párovou značku, tak i dvojici `{if} ... {/if}` lze zapisovat i v podobě [n:attributu|syntax#n:atributy], například: - -```latte -

    Skladem {$count} kusů

    -``` - -Víte, že k n:atributům můžete připojit prefix `tag-`? Pak se bude podmínka vztahovat jen na vypsání HTML značek a obsah mezi nimi se vypíše vždycky: - -```latte -
    Hello - -{* vypíše 'Hello' když $clickable je nepravdivá *} -{* vypíše 'Hello' když $clickable je pravdivá *} -``` - -Boží. - - -`{/if $cond}` -------------- - -Možná vás překvapí, že výraz v podmínce `{if}` lze uvést také v ukončovací značce. To se hodí v situacích, kdy při otevírání podmínky ještě jeho hodnotu neznáme. Říkejme tomu odložené rozhodnutí. - -Například začneme vypisovat tabulku se záznamy z databáze a teprve po dokončení výpisu si uvědomíme, že v databázi žádný záznam nebyl. Tak dáme na to podmínku do koncové značky `{/if}` a pokud žádný záznam nebude, nic z toho se nevypíše: - -```latte -{if} -

    Výpis řádků z databáze

    - - - {foreach $resultSet as $row} - ... - {/foreach} -
    -{/if isset($row)} -``` - -Šikovné, že? - -V odložené podmínce lze použít i `{else}`, ale nikoliv `{elseif}`. - - -`{ifset}` `{elseifset}` ------------------------ - -.[note] -Viz také [`{ifset block}` |template-inheritance#Kontrola existence bloků] - -Pomocí podmínky `{ifset $var}` zjistíme, zda proměnná (nebo více proměnných) existuje a má ne*null*ovou hodnotu. Vlastně jde o totéž, jako `if (isset($var))` v PHP. Jako každou párovou značku ji lze zapisovat i v podobě [n:attributu|syntax#n:atributy], tak si to ukažme jako příklad: - -```latte - -``` - - -`{ifchanged}` .{data-version:2.9} ---------------------------------- - -`{ifchanged}` zkontroluje, zda se hodnota proměnné změnila od poslední iterace ve smyčce (foreach, for nebo while). - -Pokud ve značce uvedeme jednu či více proměnných, bude kontrolovat, zda se nějaká z nich změnila, a podle toho vypíše obsah. Například následující příklad vypíše první písmenko jména jako nadpis pokaždé, když se při výpisu jmen změní: - -```latte -{foreach ($names|sort) as $name} - {ifchanged $name[0]}

    {$name[0]}

    {/ifchanged} - -

    {$name}

    -{/foreach} -``` - -Pokud však neuvedeme žádný argument, bude se kontrolovat vykreslený obsah oproti jeho předchozímu stavu. To znamená, že v předchozím příkladě můžeme klidně argument ve značce vynechat. A samozřejmě také můžeme použít [n:attribut|syntax#n:atributy]: - -```latte -{foreach ($names|sort) as $name} -

    {$name[0]}

    - -

    {$name}

    -{/foreach} -``` - -Uvnitř `{ifchanged}` lze také uvést klauzuli `{else}`. - - -`{switch}` `{case}` `{default}` -------------------------------- -Porovnává hodnotu s více možnostmi. Jde o obdobu podmíněnému příklazu `switch`, který znáte z PHP. Nicméně Latte jej vylepšuje: - -- používá striktní porovnání (`===`) -- nepotřebuje `break` - -Je to tedy přesný ekvivalent struktury `match` se kterou přichází PHP 8.0. - -```latte -{switch $transport} - {case train} - Vlakem - {case plane} - Letecky - {default} - Jinak -{/switch} -``` -.{data-version:2.9} -Klauzule `{case}` může obsahovat více hodnot oddělených čárkami: - -```latte -{switch $status} -{case $status::NEW}nová položka -{case $status::SOLD, $status::UNKNOWN}není dostupná -{/switch} -``` - - -Cykly -===== - -V Latte najdete všechny cykly, které znáte z PHP: foreach, for a while. - - -`{foreach}` ------------ - -Cyklus zapíšeme úplně stejně jako v PHP: - -```latte -{foreach $langs as $code => $lang} - {$lang} -{/foreach} -``` - -Navíc má několik šikovných vychytávek, o kterých si nyní povíme. - -Latte třeba kontroluje, zda vytvořené proměnné omylem nepřepisují globální proměnné téhož jména. Zachrání to situace, kdy počítáte s tím, že v `$lang` je aktuální jazyk stránky, a neuvědomíte si, že `foreach $langs as $lang` vám tu proměnnou přepsalo. - -Cyklus foreach lze také velmi elegantně a úsporně zapsat pomocí [n:attributu|syntax#n:atributy]: - -```latte -
      -
    • {$item->name}
    • -
    -``` - -Víte, že k n:atributům můžete připojit prefix `inner-`? Pak se bude ve smyčce opakovat pouze vnitřek elementu: - -```latte -
    -

    {$item->title}

    -

    {$item->description}

    -
    -``` - -Takže se vypíše něco jako: - -```latte -
    -

    Foo

    -

    Lorem ipsum.

    -

    Bar

    -

    Sit dolor.

    -
    -``` - - -`{else}` .{data-version:2.9}{toc: foreach-else} ------------------------------------------------ - -Uvnitř cyklu `foreach` může uvést klauzuli `{else}`, jejíž obsah se zobrazí, pokud je cyklus prázdný: - -```latte -
      - {foreach $people as $person} -
    • {$person->name}
    • - {else} -
    • Litujeme, v tomto seznamu nejsou žádní uživatelé
    • - {/foreach} -
    -``` - - -`$iterator` ------------ - -Uvnitř cyklu `foreach` vytvoří Latte proměnnou `$iterator`, pomocí které můžeme zjišťovat užitečné informace o probíhajícím cyklu: - -- `$iterator->first` - prochází se cyklem poprvé? -- `$iterator->last` - jde o poslední průchod? -- `$iterator->counter` - kolikátý je to průchod počítáno od jedné? -- `$iterator->counter0` - kolikátý je to průchod počítáno od nuly? .{data-version:2.9} -- `$iterator->odd` - jde o lichý průchod? -- `$iterator->even` - jde o sudý průchod? -- `$iterator->parent` - iterátor obklopující ten aktuální .{data-version:2.9} -- `$iterator->nextValue` - následující položka v cyklu -- `$iterator->nextKey` - klíč následující položky v cyklu - - -```latte -{foreach $rows as $row} - {if $iterator->first}{/if} - - - - - - - {if $iterator->last}
    {$row->name}{$row->email}
    {/if} -{/foreach} -``` - -Latte je mazané a `$iterator->last` funguje nejen u polí, ale i když cyklus probíhá nad obecným iterátorem, kde není dopředu znám počet položek. - - -`{first}` `{last}` `{sep}` --------------------------- - -Tyto značky lze používat uvnitř cyklu `{foreach}`. Obsah `{first}` se vykreslí, pokud jde o první průchod. -Obsah `{last}` se vykreslí … jestlipak to uhádnete? Ano, pokud jde o poslední průchod. Jde vlastně o zkratky pro `{if $iterator->first}` a `{if $iterator->last}`. - -Značky lze také elegantně použít jako [n:attribut|syntax#n:atributy]: - -```latte -{foreach $rows as $row} - {first}

    List of names

    {/first} - -

    {$row->name}

    - -
    -{/foreach} -``` - -Obsah značky `{sep}` se vykreslí, pokud průchod není poslední, hodí se tedy pro vykreslování oddělovačů, například čárek mezi vypisovanými položkami: - -```latte -{foreach $items as $item} {$item} {sep}, {/sep} {/foreach} -``` - -To je docela praktické, že? - - -`{iterateWhile}` .{data-version:2.10} -------------------------------------- - -Zjednodušuje seskupování lineárních dat během iterování v cyklu foreach tím, že iteraci provádí ve vnořené smyčce, dokud je splněná podmínka. [Přečtěte si návod|cookbook/iteratewhile]. - -Může také elegantně nahradit `{first}` a `{last}` v příkladu výše: - -```latte -{foreach $rows as $row} - - - {iterateWhile} - - - - - {/iterateWhile true} - -
    {$row->name}{$row->email}
    -{/foreach} -``` - - -`{for}` -------- - -Cyklus zapisujeme úplně stejně jako v PHP: - -```latte -{for $i = 0; $i < 10; $i++} - Položka {$i} -{/for} -``` - -Značku lze také použít jako [n:attribut|syntax#n:atributy]: - -```latte -

    {$i}

    -``` - - -`{while}` ---------- - -Cyklus opět zapisujeme úplně stejně jako v PHP: - -```latte -{while $row = $result->fetch()} - {$row->title} -{/while} -``` - -Nebo jako [n:attribut|syntax#n:atributy]: - -```latte - - {$row->title} - -``` - -Je možná i varianta s podmínkou v koncové značce, která odpovídá v PHP cyklu do-while: - -```latte -{while} - {$item->title} -{/while $item = $item->getNext()} -``` - - -`{continueIf}` `{skipIf}` `{breakIf}` -------------------------------------- - -Pro řízení jakéhokoliv cyklu lze používat značky `{continueIf ?}` a `{breakIf ?}`, které přejdou na další prvek resp. ukončí cyklus při splnění podmínky: - -```latte -{foreach $rows as $row} - {continueIf $row->date < $now} - {breakIf $row->parent === null} - ... -{/foreach} -``` - -.{data-version:2.9} -Značka `{skipIf}` je velmi podobná jako `{continueIf}`, ale nezvyšuje počítadlo `$iterator->counter`, takže pokud jej vypisujeme a zároveň přeskočíme některé položky, nebudou v číslování díry. A také klauzule `{else}` se vykreslí, když přeskočíme všechny položky. - -```latte -
      - {foreach $people as $person} - {skipIf $person->age < 18} -
    • {$iterator->counter}. {$person->name}
    • - {else} -
    • Litujeme, v tomto seznamu nejsou žádní dospělí
    • - {/foreach} -
    -``` - - -`{exitIf}` .{data-version:3.0.5} --------------------------------- - -Ukončí vykreslování šablony nebo bloku při splnění podmínky (tzv. "early exit"). - -```latte -{exitIf !$messages} - -

    Messages

    -
    - {$message} -
    -``` - - -Vložení šablony -=============== - - -`{include 'file.latte'}` .{toc: include} ----------------------------------------- - -.[note] -Viz také [`{include block}` |template-inheritance#Vykreslení bloků] - -Značka `{include}` načte a vykreslí uvedenou šablonu. Pokud bychom se bavili v řeči našeho oblíbeného jazyka PHP, je to něco jako: - -```php - -``` - -Vložené šablony nemají přístup k proměnným aktivního kontextu, mají přístup jen ke globálním proměnným. - -Další proměnné můžete předávat tímto způsobem: - -```latte -{* od Latte 2.9 *} -{include 'template.latte', foo: 'bar', id: 123} - -{* před Latte 2.9 *} -{include 'template.latte', foo => 'bar', id => 123} -``` - -Název šablony může být jakákoliv výraz v PHP: - -```latte -{include $someVar} -{include $ajax ? 'ajax.latte' : 'not-ajax.latte'} -``` - -Vložený obsah lze upravit pomocí [filtrů|syntax#filtry]. Následující příklad odebere všechno HTML a upraví velikost písmen: - -```latte -{include 'heading.latte' |stripHtml|capitalize} -``` - -Defaultně [dědičnost šablon|template-inheritance] v tomto případě nijak nefiguruje. I když v inkludované šabloně můžeme používat bloky, nedojde k nahrazení odpovídajících bloků v šabloně, do které se inkluduje. Přemýšlejte o inkludovaných šabloných jako samostatných odstíněných částech stránek nebo modulů. Toto chování se dá změnit pomocí modifikátoru `with blocks` (od Latte 2.9.1): - -```latte -{include 'template.latte' with blocks} -``` - -Vztah mezi názvem souboru uvedeným ve značce a souborem na disku je věcí [loaderu|extending-latte#Loadery]. - - -`{sandbox}` .{data-version:2.8} -------------------------------- - -Při vkládání šablony vytvořené koncovým uživatelem byste měli zvážit sandbox režim (více informací v [dokumentaci sandboxu |sandbox]): - -```latte -{sandbox 'untrusted.latte', level: 3, data: $menu} -``` - - -`{block}` -========= - -.[note] -Viz také [`{block name}` |template-inheritance#bloky] - -Bloky bez jména slouží jako způsob jak aplikovat [filtry|syntax#filtry] na část šablony. Například takto lze aplikovat filtr [strip|filters#strip], který odstraní zbytečné mezery: - -```latte -{block|strip} -
      -
    • Hello World
    • -
    -{/block} -``` - - -Řízení výjimek -============== - - -`{try}` .{data-version:2.9} ---------------------------- - -Díky této značce je extrémně snadné vytvářet robustní šablony. - -Pokud při vykreslování bloku `{try}` dojde k výjimce, celý blok se zahodí a vykreslování bude pokračovat až po něm: - -```latte -{try} -
      - {foreach $twitter->loadTweets() as $tweet} -
    • {$tweet->text}
    • - {/foreach} -
    -{/try} -``` - -Obsah ve volitelné klauzuli `{else}` se vykreslí jen když nastane výjimka: - -```latte -{try} -
      - {foreach $twitter->loadTweets() as $tweet} -
    • {$tweet->text}
    • - {/foreach} -
    - {else} -

    Je nám líto, nepodařilo se načíst tweety.

    -{/try} -``` - -Značku lze také použít jako [n:attribut|syntax#n:atributy]: - -```latte -
      - ... -
    -``` - -Je také možné definovat vlastní [obslužný handler pro výjimky|develop#exception handler], například kvůli logování. - - -`{rollback}` .{data-version:2.9} --------------------------------- - -Blok `{try}` lze zastavit a přeskočit také ručně pomocí `{rollback}`. Díky tomu nemusíte předem kontrolovat všechna vstupní data a až během vykreslování se můžete rozhodnout, že objekt nechcete vůbec vykreslit: - -```latte -{try} -
      - {foreach $people as $person} - {skipIf $person->age < 18} -
    • {$person->name}
    • - {else} - {rollback} - {/foreach} -
    -{/try} -``` - - -Proměnné -======== - - -`{var}` `{default}` -------------------- - -Nové proměnné vytvoříme v šabloně značkou `{var}`: - -```latte -{var $name = 'John Smith'} -{var $age = 27} - -{* Vícenásobná deklarace *} -{var $name = 'John Smith', $age = 27} -``` - -Značka `{default}` funguje podobně s tím rozdílem, že vytváří proměnné jen tehdy, pokud neexistují: - -```latte -{default $lang = 'cs'} -``` - -Od Latte 2.7 můžete uvádět i [typy proměnných|type-system]. Zatím jsou informativní a Latte je nekontroluje. - -```latte -{var string $name = $article->getTitle()} -{default int $id = 0} -``` - - -`{parameters}` .{data-version:2.9} ----------------------------------- - -Tak jako funkce deklaruje své parametry, může i šablona na začátku deklarovat své proměnné: - -```latte -{parameters - $a, - ?int $b, - int|string $c = 10 -} -``` - -Proměnné `$a` a `$b` bez uvedené výchozí hodnoty mají automaticky výchozí hodnotu `null`. Deklarované typy jsou zatím informativní a Latte je nekontroluje. - -Jiné proměnné než deklarované se do šablony nepřenášejí. Tím se liší od značky `{default}`. - - -`{capture}` ------------ - -Zachytí výstup do proměnné: - -```latte -{capture $var} -
      -
    • Hello World
    • -
    -{/capture} - -

    Captured: {$var}

    -``` - -Značku lze také zapsat jako [n:attribut|syntax#n:atributy]: - -```latte -
      -
    • Hello World
    • -
    -``` - - -Ostatní -======= - - -`{contentType}` ---------------- - -Značkou určíte, jaký typ obsahu šablona představuje. Možnosti jsou: - -- `html` (výchozí typ) -- `xml` -- `javascript` -- `css` -- `calendar` (iCal) -- `text` - -Její použití je důležité, protože nastaví [kontextově sensitivní escapování |safety-first#Kontextově sensitivní escapování] a jen tak může escapovat správně. Například `{contentType xml}` přepne do režimu XML, `{contentType text}` escapování zcela vypne. - -Pokud je parametrem plnohodnotný MIME type, jako například `application/xml`, tak ještě navíc odešle HTTP hlavičku `Content-Type` do prohlížeče: - -```latte -{contentType application/xml} - - - - RSS feed - - ... - - - -``` - - -`{debugbreak}` --------------- - -Označuje místo, kde dojde k pozastavení běhu programu a spuštění debuggeru, aby mohl programátor provést inspekci běhového prostředí a zjistit, zda program funguje podle očekávání. Podporuje [Xdebug |https://xdebug.org/]. Lze doplnit podmínku, která určuje, kdy má být program pozastaven. - -```latte -{debugbreak} {* pozastaví program *} - -{debugbreak $counter == 1} {* pozastaví program při splnění podmínky *} -``` - - -`{do}` ------- - -Vykoná kód a nic nevypisuje. - -```latte -{do $num++} -``` - -V Latte 2.7 a starších se používalo `{php}`. - - -`{dump}` --------- - -Vypíše proměnnou nebo aktuální kontext. - -```latte -{dump $name} {* Vypíše proměnnou $name *} - -{dump} {* Vypíše všechny aktuálně definované proměnné *} -``` - -.[caution] -Vyžaduje knihovnu [Tracy|tracy:]. - - -`{spaceless}` -------------- - -Odstraní zbytečné bílé místo z výstupu. Funguje podobně jako filtr [spaceless|filters#spaceless]. - -```latte -{spaceless} -
      -
    • Hello
    • -
    -{/spaceless} -``` - -Vygeneruje - -```latte -
    • Hello
    -``` - -Značku lze také zapsat jako [n:attribut|syntax#n:atributy]. - - -`{syntax}` ----------- - -Latte značky nemusejí být ohraničeny pouze do jednoduchých složených závorek. Můžeme si zvolit i jiný oddělovač a to dokonce za běhu. Slouží k tomu `{syntax …}`, kde jako parametr lze uvést: - -- double: `{{...}}` -- off: zcela vypne zpracování Latte značek - -S využitím n:atributů lze vypnout Latte třeba jen jednomu bloku JavaScriptu: - -```latte - -``` - -Latte lze velmi pohodlně používat i uvnitř JavaScriptu, jen se stačí vynout konstrukcím jako v tomto příkladě, kdy následuje písmeno hned za `{`, viz [Latte uvnitř JavaScriptu nebo CSS|recipes#Latte uvnitř JavaScriptu nebo CSS]. - -Pokud Latte vypnete pomocí `{syntax off}` (tj. značkou, nikoliv n:atributem), bude důsledně ignorovat všechny značky až do `{/syntax}` - - -{trace} .{data-version:2.10} ----------------------------- - -Vyhodí výjimku `Latte\RuntimeException`, jejíž stack trace se nese v duchu šablon. Tedy místo volání funkcí a metod obsahuje volání bloků a vkládání šablon. Pokud používáte nástroj pro přehledné zobrazení vyhozených výjimek, jako je například [Tracy|tracy:], přehledně se vám zobrazí call stack včetně všech předávaných argumentů. - - -Pomocníci HTML kodéra -===================== - - -n:class -------- - -Díky `n:class` velice snadno vygenerujete HTML atribut `class` přesně podle představ. - -Příklad: potřebuji, aby aktivní prvek měl třídu `active`: - -```latte -{foreach $items as $item} - ... -{/foreach} -``` - -A dále, aby první prvek měl třídy `first` a `main`: - -```latte -{foreach $items as $item} - ... -{/foreach} -``` - -A všechny prvky mají mít třídu `list-item`: - -```latte -{foreach $items as $item} - ... -{/foreach} -``` - -Úžasně jednoduché, že? - - -n:attr ------- - -Atribut `n:attr` umí se stejnou elegancí jako má [n:class|#n:class] generovat libovolné HTML atributy. - -```latte -{foreach $data as $item} - -{/foreach} -``` - -V závislosti na vrácených hodnotách vypíše např.: - -```latte - - - - - -``` - - -n:tag .{data-version:2.10} --------------------------- - -Atribut `n:tag` umí dynamicky měnit název HTML elementu. - -```latte -

    {$title}

    -``` - -Pokud je `$heading === null`, vypíše se beze změny tag `

    `. Jinak se změní název elementu na hodnotu proměnné, takže pro `$heading === 'h3'` se vypíše: - -```latte -

    ...

    -``` - - -n:ifcontent ------------ - -Předchází tomu, aby se vypsal prázdný HTML element, tj. element neobsahující nic než mezery. - -```latte -
    -
    {$error}
    -
    -``` - -Vypíše v závislosti na hodnotě proměnné `$error`: - -```latte -{* $error = '' *} -
    -
    - -{* $error = 'Required' *} -
    -
    Required
    -
    -``` - - -Překlady .{data-version:3.0} -============================ - -Aby značky pro překlad fungovaly, je potřeba [aktivovat překladač|develop#TranslatorExtension]. Pro překlad můžete také použít filtr [`translate`|filters#translate]. - - -`{_...}` --------- - -Překládá hodnoty do jiných jazyků. - -```latte -{_'Košík'} -{_$item} -``` - -Překladači lze předávat i další parametry: - -```latte -{_'Košík', domain: order} -``` - - -`{translate}` -------------- - -Překládá části šablony: - -```latte -

    {translate}Objednávka{/translate}

    - -{translate domain: order}Lorem ipsum ...{/translate} -``` - -Značku lze také zapsat jako [n:attribut|syntax#n:atributy], pro překlad vnitřku elementu: - -```latte -

    Objednávka

    -``` diff --git a/latte/cs/template-inheritance.texy b/latte/cs/template-inheritance.texy deleted file mode 100644 index b8a62fc2f7..0000000000 --- a/latte/cs/template-inheritance.texy +++ /dev/null @@ -1,757 +0,0 @@ -Dědičnost a znovupoužitelnost šablon -************************************ - -.[perex] -Mechanismy opětovného použití a dědičnosti šablon zvýší vaši produktivitu, protože každá šablona obsahuje pouze svůj jedinečný obsah a opakované prvky a struktury se znovupoužijí. Představujeme tři koncepty: [#layoutová dědičnost], [#horizontální znovupoužití] a [#jednotková dědičnost]. - -Koncept dědičnosti šablon Latte je podobný dědičnosti tříd v PHP. Definujete **nadřazenou šablonu**, od které mohou dědit další **podřízené šablony** a mohou přepsat části nadřazené šablony. Funguje to skvěle, když prvky sdílejí společnou strukturu. Zní to komplikovaně? Nebojte se, je to velmi snadné. - - -Layoutová dědičnost `{layout}` .{toc:Layoutová dědičnost} -========================================================= - -Podívejme se na dědičnost šablony rozložení, tedy layoutu, rovnou příkladem. Toto je nadřazená šablona, kterou budeme nazývat například `layout.latte` a která definuje kostru HTML dokumentu: - -```latte - - - - {block title}{/block} - - - -
    - {block content}{/block} -
    - - - -``` - -Značky `{block}` definují tři bloky, které mohou podřízené šablony vyplnit. Značka block dělá jen to, že oznámí, že toto místo může podřízená šablona přepsat definováním vlastního bloku se stejným názvem. - -Podřízená šablona může vypadat takto: - -```latte -{layout 'layout.latte'} - -{block title}My amazing blog{/block} - -{block content} -

    Welcome to my awesome homepage.

    -{/block} -``` - -Klíčem je zde značka `{layout}`. Říká Latte, že tato šablona „rozšiřuje“ další šablonu. Když Latte vykresluje tuto šablonu, nejprve najde rodiče - v tomto případě `layout.latte`. - -V tomto okamžiku si Latte všimne tří blokových značek v `layout.latte` a nahradí tyto bloky obsahem podřízené šablony. Vzhledem k tomu, že podřízená šablona nedefinovala blok *footer*, použije se místo toho obsah z nadřazené šablony. Obsah ve značce `{block}` v nadřazené šabloně se vždy používá jako záložní. - -Výstup může vypadat takto: - -```latte - - - - My amazing blog - - - -
    -

    Welcome to my awesome homepage.

    -
    - - - -``` - -V podřízené šabloně mohou být bloky umístěny pouze na nejvyšší úrovni nebo uvnitř jiného bloku, tj .: - -```latte -{block content} -

    {block title}Welcome to my awesome homepage{/block}

    -{/block} -``` - -Také bude vždy vytvořen blok bez ohledu na to, zda je okolní `{if}` podmínka vyhodnocena jako pravdivá nebo nepravdivá. Takže i když to tak nevypadá, tato šablona blok nadefinuje. - -```latte -{if false} - {block head} - - {/block} -{/if} -``` - -Pokud chcete, aby se výstup uvnitř bloku zobrazoval podmíněně, použijte místo toho následující: - -```latte -{block head} - {if $condition} - - {/if} -{/block} -``` - -Prostor mimo bloky v podřízené šabloně se provádí před vykreslením šablony layoutu, takže je můžete použít k definování proměnných jako `{var $foo = bar}` a k šíření dat do celého řetězce dědičnosti: - -```latte -{layout 'layout.latte'} -{var $robots = noindex} - -... -``` - - -Víceúrovňová dědičnost ----------------------- -Můžete použít tolik úrovní dědičnosti, kolik potřebujete. Běžný způsob použití layoutové dědičnosti je následující tříúrovňový přístup: - -1) Vytvořte šablonu `layout.latte`, která obsahuje hlavní kostru vzhledu webu. -2) Vytvořte šablonu `layout-SECTIONNAME.latte` pro každou sekci svého webu. Například `layout-news.latte`, `layout-blog.latte` atd. Všechny tyto šablony rozšiřují `layout.latte` a zahrnují styly & design specifické pro jednotlivé sekce. -3) Vytvořte individuální šablony pro každý typ stránky, například novinový článek nebo položku blogu. Tyto šablony rozšiřují příslušnou šablonu sekce. - - -Dynamická dědičnost -------------------- -Jako název nadřazené šablony lze použít proměnnou nebo jakýkoli výraz PHP, takže dědičnost se může chovat dynamicky: - -```latte -{layout $standalone ? 'minimum.latte' : 'layout.latte'} -``` - -Můžete také použít Latte API k [automatickému |develop#automaticke-dohledavani-layoutu] výběru šablony layoutu. - - -Tipy ----- -Zde je několik tipů pro práci s layoutovou dědičností: - -- Pokud v šabloně použijete `{layout}`, musí to být první značka šablony v této šabloně. - -- Značka `{layout}` má alias `{extends}`. - -- Název souboru layoutu závisí na [loaderu |extending-latte#Loadery]. - -- Můžete mít tolik bloků, kolik chcete. Pamatujte, že podřízené šablony nemusí definovat všechny nadřazené bloky, takže můžete vyplnit přiměřené výchozí hodnoty v několika blocích a poté definovat pouze ty, které potřebujete později. - - -Bloky `{block}` .{toc: Bloky} -============================= - -.[note] -Viz také anonymní [`{block}` |tags#block] - -Blok představuje způsob, jak změnit způsob vykreslování určité části šablony, ale nijak nezasahuje do logiky kolem něj. V následujícím příkladu si ukážeme, jak blok funguje, ale také jak nefunguje: - -```latte -{* parent.Latte *} -{foreach $posts as $post} -{block post} -

    {$post->title}

    -

    {$post->body}

    -{/block} -{/foreach} -``` - -Pokud tuto šablonu vykreslíte, bude výsledek přesně stejný se značkami `{block}` i bez nich. Bloky mají přístup k proměnným z vnějších oborů. Jen dávají možnost se nechat přepsat podřízenou šablonou: - -```latte -{* child.Latte *} -{layout 'parent.Latte'} - -{block post} -
    -
    {$post->title}
    -
    {$post->text}
    -
    -{/block} -``` - -Nyní při vykreslování podřízené šablony bude smyčka používat blok definovaný v podřízené šabloně `child.Latte` namísto bloku definovaného v `parent.Latte`; spuštěná šablona je pak ekvivalentní následující: - -```latte -{foreach $posts as $post} -
    -
    {$post->title}
    -
    {$post->text}
    -
    -{/foreach} -``` - -Pokud však vytvoříme novou proměnnou uvnitř pojmenovaného bloku nebo nahradíme hodnotu stávajícího, změna bude viditelná pouze uvnitř bloku: - -```latte -{var $foo = 'foo'} -{block post} - {do $foo = 'new value'} - {var $bar = 'bar'} -{/block} - -foo: {$foo} // prints: foo -bar: {$bar ?? 'not defined'} // prints: not defined -``` - -Obsah bloku lze upravit pomocí [filtrů |syntax#filtry]. Následující příklad odebere všechny HTML a změní velikost písmen: - -```latte -{block title|stripHtml|capitalize}...{/block} -``` - -Značku lze také zapsat jako [n:attribut|syntax#n:atributy]: - -```latte -
    - ... -
    -``` - - -Lokální bloky .{data-version:2.9} ---------------------------------- - -Každý blok přepisuje obsah nadřazeného bloku se stejným názvem – kromě lokálních bloků. Ve třídách by šlo o něco jako privátní metody. Šablonu tak můžete tvořit bez obav, že kvůli shodě jmen bloků by byly přepsány z jiné šablonoy. - -```latte -{block local helper} - ... -{/block} -``` - - -Vykreslení bloků `{include}` .{toc: Vykreslení bloků} ------------------------------------------------------ - -.[note] -Viz také [`{include file}` |tags#include] - -Chcete-li blok vypsat na určitém místě, použijte značku `{include blockname}`: - -```latte -{block title}{/block} - -

    {include title}

    -``` - -Lze také vypsat blok z jiné šablony: - -```latte -{include footer from 'main.latte'} -``` - -Vykreslovaný blok nemá přístup k proměnným aktivního kontextu, kromě případů, kdy je blok definován ve stejném souboru, kde je i vložen. Má však přístup ke globálním proměnným. - -Proměnné můžete předávat tímto způsobem: - -```latte -{* od Latte 2.9 *} -{include footer, foo: bar, id: 123} - -{* před Latte 2.9 *} -{include footer, foo => bar, id => 123} -``` - -Jako název bloku lze použít proměnnou nebo jakýkoli výraz v PHP. V takovém případě před proměnnou ještě doplníme klíčové slovo `block`, aby už v době kompilace Latte vědělo, že jde blok, a nikoliv o [vkládání šablony|tags#include], jejíž název by také mohl být v proměnné: - -```latte -{var $name = footer} -{include block $name} -``` - -Blok lze vykreslit i uvnitř sebe samého, což je například užitečné při vykreslování stromové struktury: - -```latte -{define menu, $items} -
      - {foreach $items as $item} -
    • - {if is_array($item)} - {include menu, $item} - {else} - {$item} - {/if} -
    • - {/foreach} -
    -{/define} -``` - -Místo `{include menu, ...}` pak můžeme napsat `{include this, ...}`, kde `this` znamená aktuální blok. - -Vykreslovaný blok lze upravit pomocí [filtrů |syntax#filtry]. Následující příklad odebere všechna HTML a změní velikost písmen: - -```latte -{include heading|stripHtml|capitalize} -``` - - -Rodičovský blok ---------------- - -Pokud potřebujete vypsat obsah bloku z nadřazené šablony, použijte `{include parent}`. To je užitečné, pokud chcete jen doplnit obsah nadřazeného bloku místo jeho úplného přepsání. - -```latte -{block footer} - {include parent} - GitHub - Twitter -{/block} -``` - - -Definice `{define}` .{toc: Definice} ------------------------------------- - -Kromě bloků existují v Latte také „definice“. V běžných programovacích jazycích bychom je přirovnali k funkcím. Jsou užitečné k opětovnému použití fragmentů šablony, abyste se neopakovali. - -Latte se snaží dělat věci jednoduše, takže v zásadě jsou definice stejné jako bloky a **všechno, co je o blocích řečeno, platí také pro definice**. Liší se od bloků pouze třemi způsoby: - -1) mohou přijímat argumenty -2) nemohou mít [filtry |syntax#filtry] -3) jsou uzavřeny ve značkách `{define}` a obsah uvnitř těchto značek se neodesílá na výstup, dokud je nevložíte. Díky tomu je můžete vytvořit kdekoli: - -```latte -{block foo}

    Hello

    {/block} -{* prints:

    Hello

    *} - -{define bar}

    World

    {/define} -{* prints nothing *} - -{include bar} -{* prints:

    World

    *} -``` - -Představte si, že máte obecnou pomocnou šablonu, která definuje, jak vykreslit formuláře HTML pomocí definic: - -```latte -{* forms.latte *} -{define input, $name, $value, $type = 'text'} - -{/define} - -{define textarea, $name, $value} - -{/define} -``` - -Argumenty jsou vždy volitelné s výchozí hodnotou `null`, pokud není uvedena výchozí hodnota (zde `text` je výchozí hodnota pro `$type`, funguje od Latte 2.9.1). Od Latte 2.7 lze deklarovat také typy parametrů: `{define input, string $name, ...}`. - -Definice nemají přístup k proměnným aktivního kontextu, ale mají přístup k globálním proměnným. - -Vkládají se [stejným způsobem jako blok |#Vykreslení bloků]: - -```latte -

    {include input, 'password', null, 'password'}

    -

    {include textarea, 'comment'}

    -``` - - -Dynamické názvy bloků ---------------------- - -Latte dovoluje velkou flexibilitu při definování bloků, protože název bloku může být jakýkoli výraz PHP. Tento příklad definuje tři bloky s názvy `hi-Peter`, `hi-John` a `hi-Mary`: - -```latte -{* parent.latte *} -{foreach [Peter, John, Mary] as $name} - {block "hi-$name"}Hi, I am {$name}.{/block} -{/foreach} -``` - -V podřízené šabloně pak můžeme předefinovat například jen jeden blok: - -```latte -{* child.latte *} -{block hi-John}Hello. I am {$name}.{/block} -``` - -Takže výstup bude vypadat takto: - -```latte -Hi, I am Peter. -Hello. I am John. -Hi, I am Mary. -``` - - -Kontrola existence bloků `{ifset}` .{toc: Kontrola existence bloků} -------------------------------------------------------------------- - -.[note] -Viz také [`{ifset $var}` |tags#ifset-elseifset] - -Pomocí testu `{ifset blockname}` zkontrolujeme, zda v aktuálním kontextu blok (nebo více bloků) existuje: - -```latte -{ifset footer} - ... -{/ifset} - -{ifset footer, header, main} - ... -{/ifset} -``` - -Jako název bloku lze použít proměnnou nebo jakýkoli výraz v PHP. V takovém případě před proměnnou ještě doplníme klíčové slovo `block`, aby bylo jasné, že nejde o test existence [proměnných|tags#ifset-elseifset]: - -```latte -{ifset block $name} - ... -{/ifset} -``` - - -Tipy ----- -Několik tipů pro práci s bloky: - -- Poslední blok nejvyšší úrovně nemusí mít uzavírací značku (blok končí koncem dokumentu). To zjednodušuje psaní podřízených šablon, které obsahují jeden primární blok. - -- Pro lepší čitelnost můžete název bloku uvést ve značce `{/block}`, například `{/block footer}`. Název se však musí shodovat s názvem bloku. Ve větších šablonách vám tato technika pomůže zjistit, které značky bloku se zavírají. - -- Ve stejné šabloně nemůžete přímo definovat více značek bloků se stejným názvem. Toho však lze dosáhnout pomocí [dynamických názvů bloků|#dynamické názvy bloků]. - -- Můžete použít [n:atributy |syntax#n:atributy] k definování bloků jako `

    Welcome to my awesome homepage

    ` - -- Bloky lze také použít bez názvů pouze k použití [filtrů|syntax#filtry]: `{block|strip} hello {/block}` - - -Horizontální znovupoužití `{import}` .{toc: Horizontální znovupoužití} -====================================================================== - -Horizontální znovupoužití je v Latte třetím mechanismem opětovné použitelnosti a dědičnosti. Umožňuje načíst bloky z jiných šablon. Je to podobné jako vytvoření souboru PHP s pomocnými funkcemi. - -I když je layoutová dědičnost šablony jednou z nejmocnějších funkcí Latte, je omezena na jednoduchou dědičnost; Šablona může rozšířit pouze jednu další šablonu. Díky tomuto omezení je layoutová dědičnost snadno srozumitelná a snadno laditelná: - -```latte -{layout 'layout.latte'} - -{block title}...{/block} -{block content}...{/block} -``` - -Horizontální znovupoužití je způsob, jak dosáhnout vícenásobné dědičnosti, ale bez složitosti: - -```latte -{layout 'layout.latte'} - -{import 'blocks.latte'} - -{block title}...{/block} -{block content}...{/block} -``` - -Příkaz `{import}` říká Latte, aby importoval všechny bloky a [#definice] definované v `blocks.latte` do aktuální šablony. - -```latte -{* blocks.latte *} - -{block sidebar}...{/block} -``` - -V tomto příkladu příkaz `{import}` importuje blok `sidebar` do hlavní šablony. - -Importovaná šablona nesmí [rozšiřovat|#Layoutová dědičnost] další šablonu a její tělo by mělo být prázdné. Importovaná šablona však může importovat další šablony. - -Značka `{import}` by měla být první značkou šablony po `{layout}`. Název šablony může být jakýkoli výraz PHP: - -```latte -{import $ajax ? 'ajax.latte' : 'not-ajax.latte'} -``` - -V šabloně můžete použít tolik `{import}` příkazů, kolik chcete. Pokud dvě importované šablony definují stejný blok, vyhrává první z nich. Nejvyšší prioritu má ale hlavní šablona, která může přepsat jakýkoli importovaný blok. - -Ke všem přepsaným blokům se dá postupně dostat tím že je vložíme jako vložen jak [#rodičovský blok]: - -```latte -{layout 'base.latte'} - -{import 'blocks.latte'} - -{block sidebar} - {include parent} -{/block} - -{block title}...{/block} -{block content}...{/block} -``` - -V tomto příkladu `{include parent}` zavolá blok `sidebar` ze šablony `blocks.latte`. - - -Jednotková dědičnost `{embed}` .{toc: Jednotková dědičnost}{data-version:2.9} -============================================================================= - -Jednotková dědičnost rozšiřuje myšlenku layoutové dědičnosti na úroveň fragmentů obsahu. Zatímco layoutová dědičnost pracuje s „kostrou dokumentu“, kterou oživují podřízené šablony, jednotková dědičnost vám umožňuje vytvářet kostry pro menší jednotky obsahu a znovu je používat kdekoli chcete. - -V jednotkové dědičnosti je klíčem značka `{embed}`. Kombinuje chování `{include}` a `{layout}`. Umožňuje vložit obsah jiné šablony či bloku a volitelně předat proměnné, stejně jako v případě `{include}`. Umožňuje také přepsat libovolný blok definovaný uvnitř vložené šablony, jako při použití `{layout}`. - -Například použijeme prvek akordeon. Podívejme se na kostru prvku uloženou v šabloně `collapsible.latte`: - -```latte -
    -

    - {block title}{/block} -

    - -
    - {block content}{/block} -
    -
    -``` - -Značky `{block}` definují dva bloky, které mohou podřízené šablony vyplnit. Ano, jako v případě nadřazené šablony v layoutové dědičnosti. Vidíte také proměnnou `$modifierClass`. - -Pojďme použít náš prvek v šabloně. Tady přichází ke slovu `{embed}`. Jedná se o mimořádně výkonnou značku, která nám umožňuje dělat všechny věci: vložit obsah šablony prvku, přidat do něj proměnné a přidat do něj bloky s vlastním HTML: - -```latte -{embed 'collapsible.latte', modifierClass: my-style} - {block title} - Hello World - {/block} - - {block content} -

    Lorem ipsum dolor sit amet, consectetuer adipiscing - elit. Nunc dapibus tortor vel mi dapibus sollicitudin.

    - {/block} -{/embed} -``` - -Výstup může vypadat takto: - -```latte -
    -

    - Hello World -

    - -
    -

    Lorem ipsum dolor sit amet, consectetuer adipiscing - elit. Nunc dapibus tortor vel mi dapibus sollicitudin.

    -
    -
    -``` - -Bloky uvnitř vložených značek tvoří samostatnou vrstvu nezávislou na ostatních blocích. Proto mohou mít stejný název jako blok mimo vložení a nejsou nijak ovlivněny. Pomocí značky [include |#Vykreslení bloků] uvnitř značek `{embed}` můžete vložit bloky zde vytvořené, bloky z vložené šablony (které *nejsou* [lokální |#lokální bloky]) a také bloky z hlavní šablony, které naopak *jsou* lokální. Můžete také [importovat bloky |#horizontální znovupoužití] z jiných souborů: - -```latte -{block outer}…{/block} -{block local hello}…{/block} - -{embed 'collapsible.latte', modifierClass: my-style} - {import 'blocks.latte'} - - {block inner}…{/block} - - {block title} - {include inner} {* works, block is defined inside embed *} - {include hello} {* works, block is local in this template *} - {include content} {* works, block is defined in embedded template *} - {include aBlockDefinedInImportedTemplate} {* works *} - {include outer} {* does not work! - block is in outer layer *} - {/block} -{/embed} -``` - -Vložené šablony nemají přístup k proměnným aktivního kontextu, ale mají přístup k globálním proměnným. - -Pomocí `{embed}` lze vkládat nejen šablony, ale i jiné bloky, a tedy předchozí příklad by se dal zapsat tímto způsobem: .{data-version:2.10} - -```latte -{define collapsible} -
    -

    - {block title}{/block} -

    - ... -
    -{/define} - - -{embed collapsible, modifierClass: my-style} - {block title} - Hello World - {/block} - ... -{/embed} -``` - -Pokud do `{embed}` předáme výraz a není zřejmé, jestli jde o název bloku nebo souboru, doplníme klíčové slovo `block` nebo `file`: - -```latte -{embed block $name} ... {/embed} -``` - - -Případy použití -=============== - -V Latte existují různé typy dědičnosti a opětovného použití kódu. Pojďme si shrnout hlavní koncepty pro větší srozumitelnost: - - -`{include template}` --------------------- - -**Případ použití**: Použití `header.latte` a `footer.latte` uvnitř `layout.latte`. - -`header.latte` - -```latte - -``` - -`footer.latte` - -```latte -
    -
    Copyright
    -
    -``` - -`layout.latte` - -```latte -{include 'header.latte'} - -
    {block main}{/block}
    - -{include 'footer.latte'} -``` - - -`{layout}` ----------- - -**Případ použití**: Rozšíření `layout.latte` uvnitř `homepage.latte` a `about.latte`. - -`layout.latte` - -```latte -{include 'header.latte'} - -
    {block main}{/block}
    - -{include 'footer.latte'} -``` - -`homepage.latte` - -```latte -{layout 'layout.latte'} - -{block main} -

    Homepage

    -{/block} -``` - -`about.latte` - -```latte -{layout 'layout.latte'} - -{block main} -

    About page

    -{/block} -``` - - -`{import}` ----------- - -**Případ použití**: `sidebar.latte` v `single.product.latte` a `single.service.latte`. - -`sidebar.latte` - -```latte -{block sidebar}{/block} -``` - -`single.product.latte` - -```latte -{layout 'product.layout.latte'} - -{import 'sidebar.latte'} - -{block main}
    Product page
    {/block} -``` - -`single.service.latte` - -```latte -{layout 'service.layout.latte'} - -{import 'sidebar.latte'} - -{block main}
    Service page
    {/block} -``` - - -`{define}` ----------- - -**Případ použití**: Funkce, které předáme proměnné a něco vykreslí. - -`form.latte` - -```latte -{define form-input, $name, $value, $type = 'text'} - -{/define} -``` - -`profile.service.latte` - -```latte -{import 'form.latte'} - -
    -
    {include form-input, username}
    -
    {include form-input, password}
    -
    {include form-input, submit, Submit, submit}
    -
    -``` - - -`{embed}` ---------- - -**Případ použití**: Vložení `pagination.latte` do `product.table.latte` a `service.table.latte`. - -`pagination.latte` - -```latte - -``` - -`product.table.latte` - -```latte -{embed 'pagination.latte', min: 1, max: $products->count} - {block first}First Product Page{/block} - {block last}Last Product Page{/block} -{/embed} -``` - -`service.table.latte` - -```latte -{embed 'pagination.latte', min: 1, max: $services->count} - {block first}First Service Page{/block} - {block last}Last Service Page{/block} -{/embed} -``` diff --git a/latte/cs/type-system.texy b/latte/cs/type-system.texy deleted file mode 100644 index b80ce38c88..0000000000 --- a/latte/cs/type-system.texy +++ /dev/null @@ -1,76 +0,0 @@ -Typový systém -************* - -
    - -Typový systém je klíčový pro vývoj robustních aplikací. Latte přináší podporou typů i do šablon. Díky tomu, že víme, jaký datový či objektový typ je v každé proměnné, může - -- IDE správně našeptávat (viz [integrace|recipes#Editory a IDE]) -- statická analýza odhalit chyby - -Obojí zásadním způsobem zvyšuje kvalitu a pohodlí vývoje. - -
    - -.[note] -Deklarované typy jsou informativní a Latte je v tuto chvíli nekontroluje. - -Jak začít používat typy? Vytvořte si třídu šablony, např. `CatalogTemplateParameters`, reprezentující předávané parametry, jejich typy a případně i výchozí hodnoty: - -```php -class CatalogTemplateParameters -{ - public function __construct( - public string $langs, - /** @var ProductEntity[] */ - public array $products, - public Address $address, - ) {} -} - -$latte->render('template.latte', new CatalogTemplateParameters( - address: $userAddress, - lang: $settings->getLanguage(), - products: $entityManager->getRepository('Product')->findAll(), -)); -``` - -A dále na začátek šablony vložte značku `{templateType}` s plným názvem třídy (včetně namespace). To definuje, že v šabloně jsou proměnné `$langs` a `$products` včetně příslušných typů. -Typy lokálních proměnných můžete uvést pomocí značek [`{var}` |tags#var-default], `{varType}`, [`{define}` |template-inheritance#definice]. - -Od té chvíle vám může IDE správně našeptávat. - -Jak si ušetřit práci? Jak co nejsnáze napsat třídu s parametry šablony nebo značky `{varType}`? Nechte si je vygenerovat. -Od toho existuje dvojice značek `{templatePrint}` a `{varPrint}`. -Pokud je umístíte do šablony, místo běžného vykreslení se zobrazí návrh kódu třídy resp. seznam značek `{varType}`. Kód pak stačí jedním kliknutím označit a zkopírovat do projektu. - - -`{templateType}` ----------------- -Typy parametrů předávaných do šablony deklarujeme pomocí třídy: - -```latte -{templateType MyApp\CatalogTemplateParameters} -``` - - -`{varType}` ------------ -Jak deklarovat typy proměnných? K tomu slouží značky `{varType}` pro existující proměnné, nebo [`{var}` |tags#var-default]: - -```latte -{varType Nette\Security\User $user} -{varType string $lang} -``` - - -`{templatePrint}` ------------------ -Třídu si také můžete nechat vygenerovat pomocí značky `{templatePrint}`. Pokud ji umístíte na začátek šablony, místo běžného vykreslení se zobrazí návrh třídy. Kód pak stačí jedním kliknutím označit a zkopírovat do projektu. - - -`{varPrint}` ------------- -Značka `{varPrint}` vám ušetří čas se psaním. Pokud ji umístíte do šablony, místo běžného vykreslení se zobrazí návrh značek `{varType}` pro lokální proměnné. Kód pak stačí jedním kliknutím označit a zkopírovat do šablony. - -Samotné `{varPrint}` vypisuje pouze lokální proměnné, které nejsou parametry šablony. Pokud chcete vypsat všechny proměnné, použijte `{varPrint all}`. diff --git a/latte/en/@home.texy b/latte/en/@home.texy deleted file mode 100644 index 107194ced2..0000000000 --- a/latte/en/@home.texy +++ /dev/null @@ -1 +0,0 @@ -{{maintitle: Latte – The Safest & Truly Intuitive Templates for PHP}} diff --git a/latte/en/@left-menu.texy b/latte/en/@left-menu.texy deleted file mode 100644 index 0b903a9480..0000000000 --- a/latte/en/@left-menu.texy +++ /dev/null @@ -1,25 +0,0 @@ -- [Getting Started |Guide] -- Concepts - - [Safety First] - - [Template Inheritance] - - [Type System] - - [Sandbox] - -- For Designers - - [Syntax] - - [Tags] - - [Filters] - - [Functions] - - [Tips and Tricks |recipes] - -- For Developers - - [Practices for Developers |develop] - - [Extending Latte] - - [Creating an Extension |creating-extension] - -- [Cookbook |cookbook/@home] - - [Migration from Twig |cookbook/migration-from-twig] - - [Migration from Latte 2 |cookbook/migration-from-latte2] - - [… more |cookbook/@home] - -- "Playground .[link-external]":https://fiddle.nette.org/latte/ .{padding-top:1em} diff --git a/latte/en/@menu.texy b/latte/en/@menu.texy deleted file mode 100644 index c7cb62830e..0000000000 --- a/latte/en/@menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -
      -- [Home |@home] -- [Documentation |Guide] -- "GitHub .[link-external]":https://github.com/nette/latte - -
    diff --git a/latte/en/cookbook/@home.texy b/latte/en/cookbook/@home.texy deleted file mode 100644 index 3b989ed46c..0000000000 --- a/latte/en/cookbook/@home.texy +++ /dev/null @@ -1,11 +0,0 @@ -Cookbook -******** - -- [Everything You Always Wanted to Know About {iterateWhile} |iteratewhile] -- [How to write SQL queries in Latte? |how-to-write-sql-queries-in-latte] -- [Migration from Latte 2 |migration-from-latte2] -- [Migration from PHP |migration-from-php] -- [Migration from Twig |migration-from-twig] -- [Using Latte with Slim 4 |slim-framework] - -{{leftbar: /@left-menu}} diff --git a/latte/en/cookbook/how-to-write-sql-queries-in-latte.texy b/latte/en/cookbook/how-to-write-sql-queries-in-latte.texy deleted file mode 100644 index eead8db711..0000000000 --- a/latte/en/cookbook/how-to-write-sql-queries-in-latte.texy +++ /dev/null @@ -1,43 +0,0 @@ -How to Write SQL Queries in Latte? -********************************** - -.[perex] -Latte can also be useful for generating really complex SQL queries. - -If the creation of a SQL query contains many conditions and variables, it can be really clearer to write it in Latte. A very simple example: - -```latte -SELECT users.* FROM users - LEFT JOIN users_groups ON users.user_id = users_groups.user_id - LEFT JOIN groups ON groups.group_id = users_groups.group_id - {ifset $country} LEFT JOIN country ON country.country_id = users.country_id {/ifset} -WHERE groups.name = 'Admins' {ifset $country} AND country.name = {$country} {/ifset} -``` - -Using `$latte->setContentType()` we tell Latte to treat the content as plain text (not as HTML) and -then we prepare an escaping function that escapes strings directly by the database driver: - -```php -$db = new PDO(/* ... */); - -$latte = new Latte\Engine; -$latte->setContentType(Latte\ContentType::Text); -$latte->addFilter('escape', fn($val) => match (true) { - is_string($val) => $db->quote($val), - is_int($val), is_float($val) => (string) $val, - is_bool($val) => $val ? '1' : '0', - is_null($val) => 'NULL', - default => throw new Exception('Unsupported type'), -}); -``` - -The usage would look like this: - -```php -$sql = $latte->renderToString('query.sql.latte', ['country' => $country]); -$result = $db->query($sql); -``` - -*This example requires Latte v3.0.5 or higher.* - -{{leftbar: /@left-menu}} diff --git a/latte/en/cookbook/iteratewhile.texy b/latte/en/cookbook/iteratewhile.texy deleted file mode 100644 index 695a0e1406..0000000000 --- a/latte/en/cookbook/iteratewhile.texy +++ /dev/null @@ -1,214 +0,0 @@ -Everything You Always Wanted to Know About {iterateWhile} -********************************************************* - -.[perex] -The tag `{iterateWhile}` is suitable for various tricks in foreach cycles. - -Suppose we have the following database table, where the items are divided into categories: - -| id | catId | name -|------------------ -| 1 | 1 | Apple -| 2 | 1 | Banana -| 3 | 2 | PHP -| 4 | 3 | Green -| 5 | 3 | Red -| 6 | 3 | Blue - -Of course, drawing items in a foreach loop as a list is easy: - -```latte -
      -{foreach $items as $item} -
    • {$item->name}
    • -{/foreach} -
    -``` - -But what to do if you want render each category in a separate list? In other words, how to solve the task of grouping items from a linear list in a foreach cycle. The output should look like this: - -```latte -
      -
    • Apple
    • -
    • Banana
    • -
    - -
      -
    • PHP
    • -
    - -
      -
    • Green
    • -
    • Red
    • -
    • Blue
    • -
    -``` - -We will show you how easily and elegantly the task can be solved with iterateWhile: - -```latte -{foreach $items as $item} -
      - {iterateWhile} -
    • {$item->name}
    • - {/iterateWhile $item->catId === $iterator->nextValue->catId} -
    -{/foreach} -``` - -While `{foreach}` marks the outer part of the cycle, ie the drawing of lists for each category, the tags `{iterateWhile}` indicate the inner part, ie the individual items. -The condition in the end tag says that the repetition will continue as long as the current and the next element belong to the same category (`$iterator->nextValue` is [next item |/tags#$iterator]). - -If the condition is always met, then all elements are drawn in the inner cycle: - -```latte -{foreach $items as $item} -
      - {iterateWhile} -
    • {$item->name} - {/iterateWhile true} -
    -{/foreach} -``` - -The result will look like this: - -```latte -
      -
    • Apple
    • -
    • Banana
    • -
    • PHP
    • -
    • Green
    • -
    • Red
    • -
    • Blue
    • -
    -``` - -What good is such an use of iterateWhile? How it differs from the solution we showed at the very beginning of this tutorial? The difference is that if the table is empty and does not contain any elements, it will not render empty `
      `. - - -Solution Without `{iterateWhile}` ---------------------------------- - -If we solved the same task with completely basic constructions of template systems, for example in Twig, Blade, or pure PHP, the solution would look something like this: - -```latte -{var $prevCatId = null} -{foreach $items as $item} - {if $item->catId !== $prevCatId} - {* the category has changed *} - - {* we close the previous
        , if it is not the first item *} - {if $prevCatId !== null} -
      - {/if} - - {* we will open a new list *} -
        - - {do $prevCatId = $item->catId} - {/if} - -
      • {$item->name}
      • -{/foreach} - -{if $prevCatId !== null} - {* we close the last list *} -
      -{/if} -``` - -However, this code is incomprehensible and unintuitive. The connection between the opening and closing HTML tags is not clear at all. It is not clear at first glance if there is a mistake. And it requires auxiliary variables like `$prevCatId`. - -In contrast, the solution with `{iterateWhile}` is clean, clear, does not need auxiliary variables and is foolproof. - - -Condition in the Closing Tag ----------------------------- - -If we specify a condition in the opening tag `{iterateWhile}`, the behavior changes: the condition (and the advance to the next element) is executed at the beginning of the inner cycle, not at the end. -Thus, while `{iterateWhile}` without condition is always entered, `{iterateWhile $cond}` is entered only when condition `$cond` is met. At the same time, the following element is written to `$item`. - -This is useful, for example, in a situation where you want to render the first element in each category in a different way, such as: - -```latte -

      Apple

      -
        -
      • Banana
      • -
      - -

      PHP

      -
        -
      - -

      Green

      -
        -
      • Red
      • -
      • Blue
      • -
      -``` - -Lets modify the original code, we draw first item and then additional items from the same category in the inner loop `{iterateWhile}`: - -```latte -{foreach $items as $item} -

      {$item->name}

      -
        - {iterateWhile $item->catId === $iterator->nextValue->catId} -
      • {$item->name}
      • - {/iterateWhile} -
      -{/foreach} -``` - - -Nested Loops ------------- - -We can create multiple inner loops in one cycle and even nest them. In this way, for example, subcategories could be grouped. - -Suppose there is another column in the table `subCatId` and in addition to each category being in a separate `
        `, each subcategory will be in a separate `
          `: - -```latte -{foreach $items as $item} -
            - {iterateWhile} -
              - {iterateWhile} -
            1. {$item->name} - {/iterateWhile $item->subCatId === $iterator->nextValue->subCatId} -
            - {/iterateWhile $item->catId === $iterator->nextValue->catId} -
          -{/foreach} -``` - - -Filter |batch -------------- - -The grouping of linear items is also provided by a filter `batch`, into batches with a fixed number of elements: - -```latte -
            -{foreach ($items|batch:3) as $batch} - {foreach $batch as $item} -
          • {$item->name}
          • - {/foreach} -{/foreach} -
          -``` - -It can be replaced with iterateWhile as follows: - -```latte -
            -{foreach $items as $item} - {iterateWhile} -
          • {$item->name}
          • - {/iterateWhile $iterator->counter0 % 3} -{/foreach} -
          -``` - -{{leftbar: /@left-menu}} diff --git a/latte/en/cookbook/migration-from-latte2.texy b/latte/en/cookbook/migration-from-latte2.texy deleted file mode 100644 index 430af298d3..0000000000 --- a/latte/en/cookbook/migration-from-latte2.texy +++ /dev/null @@ -1,285 +0,0 @@ -Migration from Latte v2 to v3 -***************************** - -.[perex] -Latte 3 has a completely rewritten compiler and a formally well-defined grammar. This should match Latte 2 as closely as possible, but there are some constructs that need minor tweaking. - -In practice, it turns out that the vast majority of the templates do not need any modification and work the same in Latte 2 as they do in Latte 3. But how to detect incompatibilities? - -**First, install the transition version Latte 2.11.** - -This version doesn't bring any new features, it just provides a warning using E_USER_DEPRECATED for cases it knows the new Latte won't support, and more importantly advises you how to fix them. To go through all the templates and test if they are compatible, you can use the [Linter|/develop#linter] tool that you run from the console: - -```shell -vendor/bin/latte-lint -``` - -Once you have resolved possible incompatibilities, upgrade to Latte 3.0. **And run Linter again** to make sure the new strict parser really understands all templates. - - -API Changes -=========== - -The API changes only apply to adding custom tags. The rest of the API remains the same as version 2, i.e. the same way to render templates, pass parameters, register filters. - -The exception is the replacement of the so-called dynamic filter `Engine::addFilter(null, ...)` with [filter loader |/extending-latte#Filter Loader], which differs in that it always returns callable and is registered with the `Engine::addFilterLoader()` method. - -The API for adding custom tags is completely different, so add-ons designed for Latte 2 won't work with it. See also [#Updates to add-ons]. - - -Syntax Changes -============== - -The changes are as follows: - -- filters use a comma as parameter separator, previously `|filter: arg : arg` is now `|filter: arg, arg` -- the `{label foo}...{/label}` tag is always paired, unpaired should be written `{label /}` -- on the other hand, the `{_'text'}` tag is always unpaired, the paired `{_}...{/}` is replaced by the new `{translate}...{/translate}` -- pseudo-strings such as `{block foo-$var}` need to be written in quotes `{block "foo-$var"}` or add compound brackets `{block foo-{$var}}` -- this also applies to attributes, i.e. instead of `n:block="foo-$var"` use `n:block="foo-{$var}"`. -- it is necessary to be case sensitive for filters in Latte 3 -- The `{do ...}` or `{php ...}` tag can only contain expressions, to use any PHP register [RawPhpExtension |/develop#RawPhpExtension]. - -And more edge cases: - -- the `n:inner-xxx`, `n:tag-xxx` and `n:ifcontent` attributes cannot be used on void HTML elements -- the `n:inner-snippet` attribute must be written without inner- -- the tags `` and `` must be terminated -- the magic variable `$iterations` has been removed (not to be confused with `$iterator`!) -- replace the `{includeblock file.latte}` tag with [`{include file.latte with blocks}`|/tags#include] or [`{import}`|/template-inheritance#horizontal-reuse] -- `{include "abc"}` should be written as `{include file "abc"}` unless `"abc"` contains a period and it is clear that it is a file - - -Updates to Add-Ons -================== - -With the complete rewrite of the parser, the way to write custom tags has completely changed. If you have custom tags created for Latte, you will need to re-write them for version 3, see [documentation|/creating-extension]. - -If you are using a foreign add-on that adds tags, you will need to wait until the author releases a version for Latte 3. The `nette/application`, `nette/caching` and `nette/forms` libraries in version 3.1, as well as Texy, have already been updated and work with both Latte 2 and 3. - - -nette/application ------------------ - -.[note] -When using Nette normally, this extension is set automatically and there is no need to change anything. - -Old code for Latte 2: - -```php -$latte->onCompile[] = function ($latte) { - Nette\Bridges\ApplicationLatte\UIMacros::install($latte->getCompiler()); -}; - -$latte->addProvider('uiControl', $control); -$latte->addProvider('uiPresenter', $control->getPresenter()); -``` - -New code for Latte 3: - -```php -$latte->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension($control)); -``` - -UIExtension adds `n:href`, `{link}`, `{control}`, `{snippet}`, etc. The tags for snippets are thus moved from Latte itself to the `nette/application` library. In Latte 3, the presenter method `templatePrepareFilters()` is no longer called. - - -nette/forms ------------ - -.[note] -When using Nette normally, this extension is set automatically and there is no need to change anything. - -Old code for Latte 2: - -```php -$latte->onCompile[] = function ($latte) { - Nette\Bridges\FormsLatte\FormMacros::install($latte->getCompiler()); -}; -``` - -New code for Latte 3: - -```php -$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); -``` - - -nette/caching -------------- - -.[note] -When using Nette normally, this extension is set automatically and there is no need to change anything. - -Old code for Latte 2: - -```php -$latte->onCompile[] = function ($latte) { - $latte->getCompiler()->addMacro('cache', new Nette\Bridges\CacheLatte\CacheMacro); -}; - -$latte->addProvider('cacheStorage', $cacheStorage); -``` - -New code for Latte 3: - -```php -$latte->addExtension(new Nette\Bridges\CacheLatte\CacheExtension($cacheStorage)); -``` - - -Translations ------------- - -TranslatorExtension adds the translation tags `{_'text'}`, new pair `{translate}...{/translate}` and the `|translate` filter. - -Old code for Latte 2: - -```php -$latte->addFilter('translate', [$translator, 'translate']); -``` - -New code for Latte 3: - -```php -$latte->addExtension(new Latte\Essential\TranslatorExtension($translator)); -``` - -In presenters, it is automatically activated by setting the translator to the template using the `$template->setTranslator($translator)` method. Without this, the translation tags will not be available and you need to register the extension manually or with a configuration file. - - -Configuration File -================== - -In Latte 2 it was possible to register new tags using [config file |application:configuration#Latte] in the `latte › macros` section. In version 3, entire extensions are added this way: - -```neon -latte: - extensions: - - App\Templating\LatteExtension - - Latte\Essential\TranslatorExtension -``` - - -Do You Develop Add-on for Latte? -================================ - -You can have support for both versions of Latte in your library at the same time. To detect the version, it is best to use the `Latte\Engine::VERSION` constant to separate the use of `onCompile[]` and `addMacro()` from the new `addExtension()`: - -```php -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - // Latte 2 initialization - $this->latte->onCompile[] = function ($latte) { - $latte->addMacro(/* ... */); - }; -} else { - // Latte 3 initialization - $this->latte->addExtension(/* ... */); -} -``` - -As an example, let's try rewriting the following code intended for Latte 2 into a form for Latte 3: - -```php -// old code for Latte 2 -$this->latte->onCompile[] = function (Latte\Engine $latte) { - $set = new Latte\Macros\MacroSet($latte->getCompiler()); - $set->addMacro('foo', 'echo %escape(MyClass:myFunc(%node.word, %node.array))'); -}; -``` - -Latte 3 is extended using [extensions|/creating-extension]. A trivial extension adding the `foo` tag would look like this: - -```php -// new code for Latte 3 -class FooExtension extends Latte\Extension -{ - public function getTags(): array - { - return [ - 'foo' => [FooNode::class, 'create'], // we will add the FooNode class in a moment - ]; - } -} - -// registration -$this->latte->addExtension(new FooExtension); -``` - -The new compiler is more robust, it doesn't include the previous shortcuts, so it takes a bit more lines of code to write a macro. For example, we can't directly pass a string of PHP code as in Latte 2, instead we create a function. Recall that in Latte 2 the function would look something like this: - -```php -// Latte 2 -$set->addMacro('foo', function (Latte\MacroNode $node, Latte\PhpWriter $writer) { - return $writer->write('echo %escape(MyClass:myFunc(%node.word, %node.array))'); -}); -``` - -Nevertheless, Latte 3 goes about it in much the same way, only `MacroNode` is called `Latte\Compiler\Tag` and `PhpWriter` is `Latte\Compiler\PrintContext`. But most importantly, there is an extra intermediate step, which is that the function does not return PHP code directly, but returns a node, i.e. a child of `StatementNode`, which is then part of the AST tree. And this node has a method `print(Latte\Compiler\PrintContext $content): string` that returns PHP code: - -```php -// Latte 3 -class FooNode extends Latte\Compiler\Nodes\StatementNode -{ - public static function create(Latte\Compiler\Tag $tag): self - { - $node = new self; - return $node; - } - - public function print(Latte\Compiler\PrintContext $context): string - { - return $context->format('echo ...'); // returns PHP code - } -} -``` - -Furthermore, the mask in `$context->format()` no longer has `%node.***` abbreviations, it is assumed that you [parse the tag content |/creating-extension#Tag Parsing Function] first. So we use the parser to parse the content into variables (subnodes), and then we write it out: - -```php -use Latte\Compiler\Nodes\Php\Expression\ArrayNode; -use Latte\Compiler\Nodes\Php\ExpressionNode; - -class FooNode extends Latte\Compiler\Nodes\StatementNode -{ - public ExpressionNode $subject; - public ArrayNode $args; - - public static function create(Latte\Compiler\Tag $tag): self - { - $node = new self; - // parsing the content of the tag - $node->subject = $tag->parser->parseUnquotedStringOrExpression(); - $tag->parser->stream->tryConsume(','); - $node->args = $tag->parser->parseArguments(); - return $node; - } - - public function print(Latte\Compiler\PrintContext $context): string - { - return $context->format( - 'echo %escape(MyClass:myFunc(%node, %node));', - $this->subject, - $this->args, - ); - } -} -``` - -Finally, we will add the `getIterator()` method to allow subnodes to be traversed when [traversing |/creating-extension#Node Traverser]: - -```php -class FooNode extends Latte\Compiler\Nodes\StatementNode -{ - ... - - public function &getIterator(): \Generator - { - yield $this->subject; - yield $this->args; - } -} -``` - -{{priority: -1}} -{{leftbar: /@left-menu}} diff --git a/latte/en/cookbook/migration-from-php.texy b/latte/en/cookbook/migration-from-php.texy deleted file mode 100644 index ebc083b433..0000000000 --- a/latte/en/cookbook/migration-from-php.texy +++ /dev/null @@ -1,72 +0,0 @@ -Migration from PHP to Latte -*************************** - -.[perex] -Are you migrating an old project written in pure PHP to Latte? We have a tool to make the migration easier. Try it out [online |https://php2latte.nette.org]. - -You can download the tool from [GitHub|https://github.com/nette/latte-tools] or install it using Composer: - -```shell -composer create-project latte/tools -``` - -The converter does not use simple regular expression substitutions, instead it uses the PHP parser directly, so it can handle any complex syntax. - -The script `php-to-latte.php` is used to convert from PHP to Latte: - -```shell -php-to-latte.php input.php [output.latte] -``` - - -Example -------- - -The input file might look like this (it is part of the PunBB forum code): - -```php -

          - -
          -
          -
          -' - . htmlspecialchars($cur_group['g_title']) . ''; - } else { - echo "\n\t\t" . ''; - } -} -?> - -

          -
          -
          -
          -``` - -Generates this template: - -```latte -

          {$lang_common['User list']}

          - -
          -
          -
          -{foreach $result as $cur_group} - {if $cur_group[g_id] == $show_group} - - {else} - - {/if} -{/foreach} -

          {$lang_ul['User search info']}

          -
          -
          -
          -``` - -{{leftbar: /@left-menu}} diff --git a/latte/en/cookbook/migration-from-twig.texy b/latte/en/cookbook/migration-from-twig.texy deleted file mode 100644 index 6ed49c9374..0000000000 --- a/latte/en/cookbook/migration-from-twig.texy +++ /dev/null @@ -1,81 +0,0 @@ -Migration from Twig to Latte -**************************** - -.[perex] -Are you migrating a project written in Twig to the more modern Latte? We have a tool to make the migration easier. Try it out [online |https://twig2latte.nette.org]. - -You can download the tool from [GitHub|https://github.com/nette/latte-tools] or install it using Composer: - -```shell -composer create-project latte/tools -``` - -The converter doesn't use simple regular expression substitutions, instead it uses the Twig parser directly, so it can handle any complex syntax. - -A script `twig-to-latte.php` is used to convert from Twig to Latte: - -```shell -twig-to-latte.php input.twig.html [output.latte] -``` - - -Conversion ----------- - -The conversion requires manual editing of the result, since the conversion cannot be done unambiguously. Twig uses dot syntax, where `{{ a.b }}` can mean `$a->b`, `$a['b']` or `$a->getB()`, which cannot be distinguished during compilation. The converter therefore converts everything to `$a->b`. - -Some functions, filters or tags have no equivalent in Latte, or may behave slightly differently. - - -Example -------- - -The input file might look like this: - -```latte -{% use "blocks.twig" %} - - - - {{ block("title") }} - - -

          {% block title %}My Web{% endblock %}

          - - - -``` - -After converting to Latte, we get this template: - -```latte -{import 'blocks.latte'} - - - - {include title} - - -

          {block title}My Web{/block}

          - - - -``` - -{{leftbar: /@left-menu}} diff --git a/latte/en/cookbook/slim-framework.texy b/latte/en/cookbook/slim-framework.texy deleted file mode 100644 index b1cd980d11..0000000000 --- a/latte/en/cookbook/slim-framework.texy +++ /dev/null @@ -1,162 +0,0 @@ -Using Latte with Slim 4 -*********************** - -.[perex] -This article written by "Daniel Opitz":https://odan.github.io/2022/04/06/slim4-latte.html describes how to use Latte with the Slim Framework. - -First, "install the Slim Framework":https://odan.github.io/2019/11/05/slim4-tutorial.html and then Latte using Composer: - -```shell -composer require latte/latte -``` - - -Configuration -------------- - -Create a new directory `templates` in your project root directory. All templates will be placed there later. - -Add a new `template` configuration key in your `config/defaults.php` file: - -```php -$settings['template'] = __DIR__ . '/../templates'; -``` - -Latte compiles the templates to native PHP code and stores them in a cache on the disk. So they are as fast as if they had been written in native PHP. - -Add a new `template_temp` configuration key in your `config/defaults.php` file: Make sure the directory `{project}/tmp/templates` exists and has read and write access permissions. - -```php -$settings['template_temp'] = __DIR__ . '/../tmp/templates'; -``` - -Latte automatically regenerates the cache every time you change the template, which can be turned off in the production environment to save a little performance: - -```php -// change to false in the production environment -$settings['template_auto_refresh'] = true; -``` - -Next, add a DI container definitions for the `Latte\Engine` class. - -```php - function (ContainerInterface $container) { - $latte = new Engine(); - $settings = $container->get('settings'); - $latte->setLoader(new FileLoader($settings['template'])); - $latte->setTempDirectory($settings['template_temp']); - $latte->setAutoRefresh($settings['template_auto_refresh']); - - return $latte; - }, -]; -``` - -This alone would technically work to render a Latte template, but we also need to make it work with the PSR-7 response object. - -For this purpose we create a special `TemplateRenderer` class which does this work for us. - -So next create a file in `src/Renderer/TemplateRenderer.php` and copy/paste this code: - -```php -engine = $engine; - } - - public function template( - ResponseInterface $response, - string $template, - array $data = [] - ): ResponseInterface - { - $string = $this->engine->renderToString($template, $data); - $response->getBody()->write($string); - - return $response; - } -} -``` - - -Usage ------ - -Instead of using the Latte Engine object directly we use the `TemplateRenderer` object to render the template into a PSR-7 compatible object. - -A typical Action handler class might look like this to render a template with the name `home.latte`: - -```php -renderer = $renderer; - } - - public function __invoke( - ServerRequestInterface $request, - ResponseInterface $response - ): ResponseInterface - { - $viewData = [ - 'items' => ['one', 'two', 'three'], - ]; - - return $this->renderer->template($response, 'home.latte', $viewData); - } -} -``` - -To make it work, create a template file in `templates/home.latte` with this content: - -```latte -
            - {foreach $items as $item} -
          • {$item|capitalize}
          • - {/foreach} -
          -``` - -If everything is configured correctly you should see the following output: - -```latte -One -Two -Three -``` - -{{priority: -1}} -{{leftbar: /@left-menu}} diff --git a/latte/en/creating-extension.texy b/latte/en/creating-extension.texy deleted file mode 100644 index f1e82a7be1..0000000000 --- a/latte/en/creating-extension.texy +++ /dev/null @@ -1,579 +0,0 @@ -Creating an Extension -********************* - -.[perex]{data-version:3.0} -An extension is a reusable class that can define custom tags, filters, functions, providers, etc. - -We create extensions when we want to reuse our Latte customizations in different projects or share them with others. -It is also useful to create an extension for each web project that will contain all the specific tags and filters you want to use in the project templates. - - -Extension Class -=============== - -Extension is a class inheriting from [api:Latte\Extension]. It is registered with Latte using `addExtension()` (or via [configuration file |application:configuration#Latte]): - -```php -$latte = new Latte\Engine; -$latte->addExtension(new MyLatteExtension); -``` - -If you register multiple extensions and they define identically named tags, filters, or functions, the last added extension wins. This also implies that your extensions can override native tags/filters/functions. - -Whenever you make a change to a class and auto-refresh is not turned off, Latte will automatically recompile your templates. - -A class can implement any of the following methods: - -```php -abstract class Extension -{ - /** - * Initializes before template is compiler. - */ - public function beforeCompile(Engine $engine): void; - - /** - * Returns a list of parsers for Latte tags. - * @return array - */ - public function getTags(): array; - - /** - * Returns a list of compiler passes. - * @return array - */ - public function getPasses(): array; - - /** - * Returns a list of |filters. - * @return array - */ - public function getFilters(): array; - - /** - * Returns a list of functions used in templates. - * @return array - */ - public function getFunctions(): array; - - /** - * Returns a list of providers. - * @return array - */ - public function getProviders(): array; - - /** - * Returns a value to distinguish multiple versions of the template. - */ - public function getCacheKey(Engine $engine): mixed; - - /** - * Initializes before template is rendered. - */ - public function beforeRender(Template $template): void; -} -``` - -For an idea of what the extension looks like, take a look at the built-in "CoreExtension":https://github.com/nette/latte/blob/master/src/Latte/Essential/CoreExtension.php. - - -beforeCompile(Latte\Engine $engine): void .[method] ---------------------------------------------------- - -Called before the template is compiled. The method can be used for compilation-related initializations, for example. - - -getTags(): array .[method] --------------------------- - -Called when the template is compiled. Returns an associative array *tag name => callable*, which are [tag parsing functions|#Tag Parsing Function]. - -```php -public function getTags(): array -{ - return [ - 'foo' => [FooNode::class, 'create'], - 'bar' => [BarNode::class, 'create'], - 'n:baz' => [NBazNode::class, 'create'], - // ... - ]; -} -``` - -The `n:baz` tag represents a pure n:attribute, i.e. it is a tag that can only be written as an attribute. - -In the case of the `foo` and `bar` tags, Latte will automatically recognize whether they are pairs, and if so, they can be written automatically using n:attributes, including variants with the `n:inner-foo` and `n:tag-foo` prefixes. - -The order of execution of such n:attributes is determined by their order in the array returned by `getTags()`. Thus, `n:foo` is always executed before `n:bar`, even if the attributes are listed in reverse order in the HTML tag as `
          `. - -If you need to determine the order of n:attributes across multiple extensions, use the `order()` helper method, where the `before` xor `after` parameter determines which tags are ordered before or after the tag. - -```php -public function getTags(): array -{ - return [ - 'foo' => self::order([FooNode::class, 'create'], before: 'bar')] - 'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])] - ]; -} -``` - - -getPasses(): array .[method] ----------------------------- - -It is called when the template is compiled. Returns an associative array *name pass => callable*, which are functions representing so-called [#compiler passes] that traverse and modify the AST. - -Again, the `order()` helper method can be used. The value of the `before` or `after` parameters can be `*` with the meaning before/after all. - -```php -public function getPasses(): array -{ - return [ - 'optimize' => [Passes::class, 'optimizePass'], - 'sandbox' => self::order([$this, 'sandboxPass'], before: '*'), - // ... - ]; -} -``` - - -beforeRender(Latte\Engine $engine): void .[method] --------------------------------------------------- - -It is called before each template rendering. The method can be used, for example, to initialize variables used during rendering. - - -getFilters(): array .[method] ------------------------------ - -It is called before the template is rendered. Returns [filters|extending-latte#filters] as an associative array *filter name => callable*. - -```php -public function getFilters(): array -{ - return [ - 'batch' => [$this, 'batchFilter'], - 'trim' => [$this, 'trimFilter'], - // ... - ]; -} -``` - - -getFunctions(): array .[method] -------------------------------- - -It is called before the template is rendered. Returns [functions|extending-latte#functions] as an associative array *function name => callable*. - -```php -public function getFunctions(): array -{ - return [ - 'clamp' => [$this, 'clampFunction'], - 'divisibleBy' => [$this, 'divisibleByFunction'], - // ... - ]; -} -``` - - -getProviders(): array .[method] -------------------------------- - -It is called before the template is rendered. Returns an array of providers, which are usually objects that use tags at runtime. They are accessed via `$this->global->...`. - -```php -public function getProviders(): array -{ - return [ - 'myFoo' => $this->foo, - 'myBar' => $this->bar, - // ... - ]; -} -``` - - -getCacheKey(Latte\Engine $engine): mixed .[method] --------------------------------------------------- - -It is called before the template is rendered. The return value becomes part of the key whose hash is contained in the name of the compiled template file. Thus, for different return values, Latte will generate different cache files. - - -How Does Latte Work? -==================== - -To understand how to define custom tags or compiler passes, it is essential to understand how Latte works under the hood. - -Template compilation in Latte simplistically works like this: - -- First, the **lexer** tokenizes the template source code into small pieces (tokens) for easier processing -- Then, the **parser** converts the stream of tokens into a meaningful tree of nodes (the Abstract Syntax Tree, AST) -- Finally, the compiler **generates** a PHP class from the AST that renders the template and caches it. - -Actually, the compilation is a bit more complicated. Latte **has two** lexers and parsers: one for the HTML template and one for the PHP-like code inside the tags. Also, the parsing doesn't run after tokenization, but the lexer and parser run in parallel in two "threads" and coordinate. It's rocket science :-) - -Furthermore, all tags have their own parsing routines. When the parser encounters a tag, it calls its parsing function (it returns [Extension::getTags()|#getTags]). -Their job is to parse the tag arguments and, in the case of paired tags, the inner content. It returns a *node* that becomes part of the AST. See [#Tag parsing function] for details. - -When the parser finishes its work, we have a complete AST representing the template. The root node is `Latte\Compiler\Nodes\TemplateNode`. The individual nodes inside the tree then represent not only the tags, but also the HTML elements, their attributes, any expressions used inside the tags, etc. - -After this, the so-called [#Compiler passes] come into play, which are functions (returned by [Extension::getPasses()|#getPasses]) that modify the AST. - -The whole process, from loading the template content, through parsing, to generating the resulting file, can be sequenced with this code, which you can experiment with and dump the intermediate results: - -```php -$latte = new Latte\Engine; -$source = $latte->getLoader()->getContent($file); -$ast = $latte->parse($source); -$latte->applyPasses($ast); -$code = $latte->generate($ast, $file); -``` - - -Example of AST --------------- - -To get a better idea of the AST, we add a sample. This is the source template: - -```latte -{foreach $category->getItems() as $item} -
        1. {$item->name|upper}
        2. - {else} - no items found -{/foreach} -``` - -And this is its representation in the form of AST: - -/--pre -Latte\Compiler\Nodes\TemplateNode( - Latte\Compiler\Nodes\FragmentNode( - - Latte\Essential\Nodes\ForeachNode( - expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode( - object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category') - name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems') - ) - value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') - content: Latte\Compiler\Nodes\FragmentNode( - - Latte\Compiler\Nodes\TextNode(' ') - - Latte\Compiler\Nodes\Html\ElementNode('li')( - content: Latte\Essential\Nodes\PrintNode( - expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode( - object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') - name: Latte\Compiler\Nodes\Php\IdentifierNode('name') - ) - modifier: Latte\Compiler\Nodes\Php\ModifierNode( - filters: - - Latte\Compiler\Nodes\Php\FilterNode('upper') - ) - ) - ) - ) - else: Latte\Compiler\Nodes\FragmentNode( - - Latte\Compiler\Nodes\TextNode('no items found') - ) - ) - ) -) -\-- - - -Custom Tags -=========== - -Three steps are needed to define a new tag: - -- defining [#tag parsing function] (responsible for parsing the tag into a node) -- creating a node class (responsible for [#generating PHP code] and [#AST traversing]) -- registering the tag using [Extension::getTags()|#getTags] - - -Tag Parsing Function --------------------- - -Parsing of tags is handled by its parsing function (the one returned by [Extension::getTags()|#getTags]). Its job is to parse and check any arguments inside the tag (it uses TagParser to do this). -Furthermore, if the tag is a pair, it will ask TemplateParser to parse and return the inner content. -The function creates and returns a node, which is usually a child of `Latte\Compiler\Nodes\StatementNode`, and this becomes part of the AST. - -We create a class for each node, which we'll do now, and elegantly place the parsing function into it as a static factory. As an example, let's try creating the familiar `{foreach}` tag: - -```php -use Latte\Compiler\Nodes\StatementNode; - -class ForeachNode extends StatementNode -{ - // a parsing function that just creates a node for now - public static function create(Latte\Compiler\Tag $tag): self - { - $node = new self; - return $node; - } - - public function print(Latte\Compiler\PrintContext $context): string - { - // code will be added later - } - - public function &getIterator(): \Generator - { - // code will be added later - } -} -``` - -The parsing function `create()` is passed an object [api:Latte\Compiler\Tag], which carries basic information about the tag (whether it is a classic tag or n:attribute, what line it is on, etc.) and mainly accesses the [api:Latte\Compiler\TagParser] in `$tag->parser`. - -If the tag must have arguments, check for their existence by calling `$tag->expectArguments()`. The methods of the `$tag->parser` object are available for parsing them: - -- `parseExpression(): ExpressionNode` for a PHP-like expression (e.g. `10 + 3`) -- `parseUnquotedStringOrExpression(): ExpressionNode` for an expression or unquoted-string -- `parseArguments(): ArrayNode` content of the array (e.g. `10, true, foo => bar`) -- `parseModifier(): ModifierNode` for a modifier (e.g. `|upper|truncate:10`) -- `parseType(): expressionNode` for typehint (e.g. `int|string` or `Foo\Bar[]`) - -and a low-level [api:Latte\Compiler\TokenStream] operating directly with tokens: - -- `$tag->parser->stream->consume(...): Token` -- `$tag->parser->stream->tryConsume(...): ?Token` - -Latte extends the PHP syntax in small ways, for example by adding modifiers, shortened ternary operators, or allowing simple alphanumeric strings to be written without quotes. This is why we use the term *PHP-like* instead of PHP. Thus, the `parseExpression()` method parses `foo` as `'foo'`, for example. -In addition, *unquoted-string* is a special case of a string that also does not need to be quoted, but at the same time does not need to be alphanumeric. For example, it is the path to a file in the `{include ../file.latte}` tag. The `parseUnquotedStringOrExpression()` method is used to parse it. - -.[note] -Studying the node classes that are part of Latte is the best way to learn all the nitty-gritty details of the parsing process. - -Let's go back to the `{foreach}` tag. In it, we expect arguments of the form `expression + 'as' + second expression`, which we parse as follows: - -```php -use Latte\Compiler\Nodes\StatementNode; -use Latte\Compiler\Nodes\Php\ExpressionNode; -use Latte\Compiler\Nodes\AreaNode; - -class ForeachNode extends StatementNode -{ - public ExpressionNode $expression; - public ExpressionNode $value; - - public static function create(Latte\Compiler\Tag $tag): self - { - $tag->expectArguments(); - $node = new self; - $node->expression = $tag->parser->parseExpression(); - $tag->parser->stream->consume('as'); - $node->value = $parser->parseExpression(); - return $node; - } -} -``` - -The expressions we have written into the variables `$expression` and `$value` represent subnodes. - -.[tip] -Define variables with subnodes as **public** so that they can be modified in [further processing steps |#Compiler Passes] if necessary. It is also necessary to **make them available** for [traversing |#AST Traversing]. - -For paired tags, like ours, the method must also let TemplateParser parse the inner contents of the tag. This is handled by `yield`, which returns a pair ''[inner content, end tag]''. We store the inner content in the `$node->content` variable. - -```php -public AreaNode $content; - -public static function create(Latte\Compiler\Tag $tag): \Generator -{ - // ... - [$node->content, $endTag] = yield; - return $node; -} -``` - -The `yield` keyword causes the `create()` method to terminate, returning control back to the TemplateParser, which continues parsing the content until it hits the end tag. It then passes control back to `create()`, which continues from where it left off. Using the `yield`, method automatically returns `Generator`. - -You can also pass an array of tag names to `yield` for which you want to stop parsing if they occur before the end tag. This helps us implement the `{foreach}...{else}...{/foreach}` construct. If `{else}` occurs, we parse the content after it into `$node->elseContent`: - -```php -public AreaNode $content; -public ?AreaNode $elseContent = null; - -public static function create(Latte\Compiler\Tag $tag): \Generator -{ - // ... - [$node->content, $nextTag] = yield ['else']; - if ($nextTag?->name === 'else') { - [$node->elseContent] = yield; - } - - return $node; -} -``` - -Returning node completes the tag parsing. - - -Generating PHP Code -------------------- - -Each node must implement the `print()` method. Returns PHP code that renders the given part of the template (runtime code). It is passed an object [api:Latte\Compiler\PrintContext] as a parameter, which has a useful `format()` method that simplifies the assembly of the resulting code. - -The `format(string $mask, ...$args)` method accepts the following placeholders in the mask: -- `%node` prints Node -- `%dump` exports the value to PHP -- `%raw` inserts the text directly without any transformation -- `%args` prints ArrayNode as arguments to the function call -- `%line` prints a comment with a line number -- `%escape(...)` escapes the content -- `%modify(...)` applies a modifier -- `%modifyContent(...)` applies a modifier to blocks - - -Our `print()` function might look like this (we neglect the `else` branch for simplicity): - -```php -public function print(Latte\Compiler\PrintContext $context): string -{ - return $context->format( - <<<'XX' - foreach (%node as %node) %line { - %node - } - - XX, - $this->expression, - $this->value, - $this->position, - $this->content, - ); -} -``` - -The `$this->position` variable is already defined by the [api:Latte\Compiler\Node] class and is set by the parser. It contains an [api:Latte\Compiler\Position] object with the position of the tag in the source code in the form of a row and column number. - -Runtime code may use auxiliary variables. To avoid collision with variables used by the template itself, it is convention to prefix them with `$ʟ__` characters. - -It can also use arbitrary values at runtime, which are passed to the template in the form of providers using the [Extension::getProviders()|#getProviders] method. It accesses them using `$this->global->...`. - - -AST Traversing --------------- - -In order to traverse the AST tree in depth, it is necessary to implement the `getIterator()` method. This will provide access to subnodes: - -```php -public function &getIterator(): \Generator -{ - yield $this->expression; - yield $this->value; - yield $this->content; - if ($this->elseContent) { - yield $this->elseContent; - } -} -``` - -Note that `getIterator()` returns a reference. This is what allows node visitors to replace individual nodes with other nodes. - -.[warning] -If a node has subnodes, it is necessary to implement this method and make all subnodes available. Otherwise, a security hole could be created. For example, sandbox mode would not be able to control subnodes and ensure that unallowed constructs are not called in them. - -Since the `yield` keyword must be present in the method body even if it has no child nodes, write it as follows: - -```php -public function &getIterator(): \Generator -{ - if (false) { - yield; - } -} -``` - - -Compiler Passes -=============== - -Compiler Passes are functions that modify ASTs or collect information in them. They are returned by the [Extension::getPasses()|#getPasses] method. - - -Node Traverser --------------- - -The most common way to work with the AST is by using a [api:Latte\Compiler\NodeTraverser]: - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; - -$ast = (new NodeTraverser)->traverse( - $ast, - enter: fn(Node $node) => ..., - leave: fn(Node $node) => ..., -); -``` - -The *enter* function (ie. visitor) is called when a node is first encountered, before its subnodes are processed. The *leave* function is called after all subnodes have been visited. -A common pattern is that *enter* is used to collect some information and then *leave* performs modifications based on that. At the time when *leave* is called, all the code inside the node will have already been visited and necessary information collected. - -How to modify AST? The easiest way is to simply change the properties of the nodes. The second way is to replace the node entirely by returning a new node. Example: the following code will change all integers in the AST to strings (e.g. 42 will be changed to `'42'`). - -```php -use Latte\Compiler\Nodes\Php; - -$ast = (new NodeTraverser)->traverse( - $ast, - leave: function (Node $node) { - if ($node instanceof Php\Scalar\IntegerNode) { - return new Php\Scalar\StringNode((string) $node->value); - } - }, -); -``` - -An AST can easily contain thousands of nodes, and traversing over all of them may be slow. In some cases, it is possible to avoid a full traversal. - -If you are looking for all `Html\ElementNode` in a tree, you know that once you've seen `Php\ExpressionNode`, there is no point in also checking all it's child nodes, because HTML cannot be inside in expressions. In this case, you can instruct the traverser to not recurse into the class node: - -```php -$ast = (new NodeTraverser)->traverse( - $ast, - enter: function (Node $node) { - if ($node instanceof Php\ExpressionNode) { - return NodeTraverser::DontTraverseChildren; - } - // ... - }, -); -``` - -If you are only looking for one specific node, it is also possible to abort the traversal entirely after finding it. - -```php -$ast = (new NodeTraverser)->traverse( - $ast, - enter: function (Node $node) { - if ($node instanceof Nodes\ParametersNode) { - return NodeTraverser::StopTraversal; - } - // ... - }, -); -``` - - -Node Helpers ------------- - -Class [api:Latte\Compiler\NodeHelpers] provides some methods which can find AST nodes that either satisfy a certain callback etc. A couple of examples are shown: - -```php -use Latte\Compiler\NodeHelpers; - -// finds all HTML element nodes -$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode); - -// finds first text node -$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode); - -// converts PHP value node to real value -$value = NodeHelpers::toValue($node); - -// converts static textual node to string -$text = NodeHelpers::toText($node); -``` diff --git a/latte/en/develop.texy b/latte/en/develop.texy deleted file mode 100644 index 0393acde7e..0000000000 --- a/latte/en/develop.texy +++ /dev/null @@ -1,299 +0,0 @@ -Practices for Developers -************************ - - -Installation -============ - -The best way how to install Latte is to use a Composer: - -```shell -composer require latte/latte -``` - -Supported PHP versions (applies to the latest patch Latte versions): - -| version | compatible with PHP -|-----------------|------------------- -| Latte 3.0 | PHP 8.0 – 8.2 -| Latte 2.11 | PHP 7.1 – 8.2 -| Latte 2.8 – 2.10| PHP 7.1 – 8.1 - - -How to Render a Template -======================== - -How to render a template? Just use this simple code: - -```php -$latte = new Latte\Engine; -// cache directory -$latte->setTempDirectory('/path/to/tempdir'); - -$params = [ /* template variables */ ]; -// or $params = new TemplateParameters(/* ... */); - -// render to output -$latte->render('template.latte', $params); -// or render to variable -$output = $latte->renderToString('template.latte', $params); -``` - -Parameters can be arrays or even better [object|#Parameters as a class], which will provide type checking and suggestion in the editor. - -.[note] -You can also find usage examples in the repository [Latte examples |https://github.com/nette-examples/latte]. - - -Performance and Caching -======================= - -Latte templates are extremely fast, because Latte compiles them directly into PHP code and caches them on disk. Thus, they have no extra overhead compared to templates written in pure PHP. - -The cache is automatically regenerated every time you change the source file. So you can conveniently edit your Latte templates during development and see the changes immediately in the browser. You can disable this feature in a production environment and save a little performance: - -```php -$latte->setAutoRefresh(false); -``` - -When deployed on a production server, the initial cache generation, especially for larger applications, can understandably take a while. Latte has built-in prevention against "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. -This is a situation where server receives a large number of concurrent requests and because Latte's cache does not yet exist, they would all generate it at the same time. Which spikes CPU. -Latte is smart, and when there are multiple concurrent requests, only the first thread generates the cache, the others wait and then use it. - - -Parameters as a Class -===================== - -Better than passing variables to the template as arrays is to create a class. You get [type-safe notation|type-system], [nice suggestion in IDE|recipes#Editors and IDE] and a way to [register filters|extending-latte#Filters Using the Class] and [functions|extending-latte#Functions Using the Class]. - -```php -class MailTemplateParameters -{ - public function __construct( - public string $lang, - public Address $address, - public string $subject, - public array $items, - public ?float $price = null, - ) {} -} - -$latte->render('mail.latte', new MailTemplateParameters( - lang: $this->lang, - subject: $title, - price: $this->getPrice(), - items: [], - address: $userAddress, -)); -``` - - -Disabling Auto-Escaping of Variable -=================================== - -If the variable contains an HTML string, you can mark it so that Latte does not automatically (and therefore double) escape it. This avoids the need to specify `|noescape` in the template. - -The easiest way is to wrap the string in a `Latte\Runtime\Html` object: - -```php -$params = [ - 'articleBody' => new Latte\Runtime\Html($article->htmlBody), -]; -``` - -Latte also does not escape all objects that implement the `Latte\HtmlStringable` interface. So you can create your own class whose `__toString()` method will return HTML code that will not be escaped automatically: - -```php -class Emphasis extends Latte\HtmlStringable -{ - public function __construct( - private string $str, - ) { - } - - public function __toString(): string - { - return '' . htmlspecialchars($this->str) . ''; - } -} - -$params = [ - 'foo' => new Emphasis('hello'), -]; -``` - -.[warning] -The `__toString` method must return correct HTML and provide parameter escaping, otherwise an XSS vulnerability may occur! - - -How to Extend Latte with Filters, Tags, etc. -============================================ - -How to add a custom filter, function, tag, etc. to Latte? Find out in the chapter [extending Latte]. -If you want to reuse your changes in different projects or if you want to share them with others, you should then [create an extension |creating-extension]. - - -Any Code in Template `{php ...}` .{data-version:3.0}{toc: RawPhpExtension} -========================================================================== - -Only PHP expressions can be written inside the [`{do}`|tags#do] tag, so you can't, for example, insert constructs like `if ... else` or semicolon-terminated statements. - -However, you can register the `RawPhpExtension` extension, which adds the `{php ...}` tag, which can be used to insert any PHP code at the template author's risk. - -```php -$latte->addExtension(new Latte\Essential\RawPhpExtension); -``` - - -Translation in Templates .{data-version:3.0}{toc: TranslatorExtension} -====================================================================== - -Use the `TranslatorExtension` extension to add [`{_...}`|tags#_], [`{translate}`|tags#translate] and filter [`translate`|filters#translate] to the template. They are used to translate values or parts of the template into other languages. The parameter is the method (PHP callable) that performs the translation: - -```php -class MyTranslator -{ - public function __construct(private string $lang) - {} - - public function translate(string $original): string - { - // create $translated from $original according to $this->lang - return $translated; - } -} - -$translator = new MyTranslator($lang); -$extension = new Latte\Essential\TranslatorExtension( - $translator->translate(...), // [$translator, 'translate'] in PHP 8.0 -); -$latte->addExtension($extension); -``` - -The translator is called at runtime when the template is rendered. However, Latte can translate all static texts during template compilation. This saves performance because each string is translated only once and the resulting translation is written to the compiled file. This creates multiple compiled versions of the template in the cache directory, one for each language. To do this, you only need to specify the language as the second parameter: - -```php -$extension = new Latte\Essential\TranslatorExtension( - $translator->translate(...), - $lang, -); -``` - -By static text we mean, for example, `{_'hello'}` or `{translate}hello{/translate}`. Non-static text, such as `{_$foo}`, will continue to be translated at runtime. - -The template can also pass additional parameters to the translator via `{_$original, foo: bar}` or `{translate foo: bar}`, which it receives as the `$params` array: - -```php -public function translate(string $original, ...$params): string -{ - // $params['foo'] === 'bar' -} -``` - - -Debugging and Tracy -=================== - -Latte tries to make the development as pleasant as possible. For debugging purposes, there are three tags [`{dump}`|tags#dump], [`{debugbreak}`|tags#debugbreak] and [`{trace}`|tags#trace]. - -You'll get the most comfort if you install the great [debugging tool Tracy|tracy:] and activate the Latte plugin: - -```php -// enables Tracy -Tracy\Debugger::enable(); - -$latte = new Latte\Engine; -// activates Tracy's extension -$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension); -``` - -You will now see all errors in a neat red screen, including errors in templates with row and column highlighting ([video|https://github.com/nette/tracy/releases/tag/v2.9.0]). -At the same time, in the bottom right corner in the so-called Tracy Bar, a tab for Latte appears, where you can clearly see all rendered templates and their relationships (including the possibility to click into the template or compiled code), as well as variables: - -[* latte-debugging.webp *] - -Since Latte compiles templates into readable PHP code, you can conveniently step through them in your IDE. - - -Linter: Validating the Template Syntax .{data-version:2.11}{toc: Linter} -======================================================================== - -The Linter tool will help you go through all templates and check for syntax errors. It is launched from the console: - -```shell -vendor/bin/latte-lint -``` - -If you use custom tags, also create your customized Linter, e.g. `custom-latte-lint`: - -```php -#!/usr/bin/env php -scanDirectory($path); - -$engine = new Latte\Engine; -// registers individual extensions here -$engine->addExtension(/* ... */); - -$path = $argv[1]; -$linter = new Latte\Tools\Linter(engine: $engine); -$ok = $linter->scanDirectory($path); -exit($ok ? 0 : 1); -``` - - -Loading Templates from a String -=============================== - -Need to load templates from strings instead of files, perhaps for testing purposes? [StringLoader|extending-latte#stringloader] will help you: - -```php -$latte->setLoader(new Latte\Loaders\StringLoader([ - 'main.file' => '{include other.file}', - 'other.file' => '{if true} {$var} {/if}', -])); - -$latte->render('main.file', $params); -``` - - -Exception Handler -================= - -You can define your own handler for expected exceptions. Exceptions raised inside [`{try}`|tags#try] and in the [sandbox] are passed to it. - -```php -$loggingHandler = function (Throwable $e, Latte\Runtime\Template $template) use ($logger) { - $logger->log($e); -}; - -$latte = new Latte\Engine; -$latte->setExceptionHandler($loggingHandler); -``` - - -Automatic Layout Lookup -======================= - -Using the tag [`{layout}`|template-inheritance#layout-inheritance], the template determines its parent template. It's also possible to have the layout searched automatically, which will simplify writing templates since they won't need to include the `{layout}` tag. - -This is achieved as follows: - -```php -$finder = function (Latte\Runtime\Template $template) { - if (!$template->getReferenceType()) { - // it returns the path to the parent template file - return 'automatic.layout.latte'; - } -}; - -$latte = new Latte\Engine; -$latte->addProvider('coreParentFinder', $finder); -``` - -If the template should not have a layout, it will indicate this with the `{layout none}` tag. diff --git a/latte/en/extending-latte.texy b/latte/en/extending-latte.texy deleted file mode 100644 index de274a756b..0000000000 --- a/latte/en/extending-latte.texy +++ /dev/null @@ -1,289 +0,0 @@ -Extending Latte -*************** - -.[perex] -Latte is very flexible and can be extended in many ways: you can add custom filters, functions, tags, loaders, etc. We will show you how to do it. - -This chapter describes the different ways to extend Latte. If you want to reuse your changes in different projects or if you want to share them with others, you should then [create so-called extension |creating-extension]. - - -How Many Roads Lead to Rome? -============================ - -Since some of the ways of extending Latte can be blended, let's first try to explain the differences between them. As an example, let's try to implement a *Lorem ipsum* generator, which is passed the number of words to generate. - -The main Latte language construct is the tag. We can implement a generator by extending Latte with a new tag: - -```latte -{lipsum 40} -``` - -The tag will work great. However, the generator in the form of a tag may not be flexible enough because it cannot be used in an expression. By the way, in practice, you rarely need to generate tags; and that's good news, because tags are a more complicated way to extend. - -Okay, let's try creating a filter instead of a tag: - -```latte -{=40|lipsum} -``` - -Again, a valid option. But the filter should transform the passed value into something else. Here we use the value `40`, which indicates the number of words generated, as the filter argument, not as the value we want to transform. - -So let's try using function: - -```latte -{lipsum(40)} -``` - -That's it! For this particular example, creating a function is the ideal extension point to use. You can call it anywhere where an expression is accepted, for example: - -```latte -{var $text = lipsum(40)} -``` - - -Filters -======= - -Create a filter by registering its name and any PHP callable, such as a function: - -```php -$latte = new Latte\Engine; -$latte->addFilter('shortify', function (string $s): string { - return mb_substr($s, 0, 10); // shortens the text to 10 characters -}); -``` - -In this case it would be better for the filter to get an additional parameter: - -```php -$latte->addFilter('shortify', function (string $s, int $len = 10): string { - return mb_substr($s, 0, $len); -}); -``` - -We use it in a template like this: - -```latte -

          {$text|shortify}

          -

          {$text|shortify:100}

          -``` - -As you can see, the function receives the left side of the filter before the pipe `|` as the first argument and the arguments passed to the filter after `:` as the next arguments. - -Of course, the function representing the filter can accept any number of parameters, and variadic parameters are also supported. - - -Filters Using the Class ------------------------ - -The second way to define a filter is to [use class|develop#Parameters as a class]. We create a method with the `TemplateFilter` attribute: - -```php -class TemplateParameters -{ - public function __construct( - // parameters - ) {} - - #[Latte\Attributes\TemplateFilter] - public function shortify(string $s, int $len = 10): string - { - return mb_substr($s, 0, $len); - } -} - -$params = new TemplateParameters(/* ... */); -$latte->render('template.latte', $params); -``` - -If you are using PHP 7.x and Latte 2.x, use the `/** @filter */` annotation instead of the attribute. - - -Filter Loader .{data-version:2.10} ----------------------------------- - -Instead of registering individual filters, you can create a so-called loader, which is a function that is called with the filter name as an argument and returns its PHP callable, or null. - -```php -$latte->addFilterLoader([new Filters, 'load']); - - -class Filters -{ - public function load(string $filter): ?callable - { - if (in_array($filter, get_class_methods($this))) { - return [$this, $filter]; - } - return null; - } - - public function shortify($s, $len = 10) - { - return mb_substr($s, 0, $len); - } - - // ... -} -``` - - -Contextual Filters ------------------- - -A contextual filter is one that accepts an object [api:Latte\Runtime\FilterInfo] in the first parameter, followed by other parameters as in the case of classical filters. It is registered in the same way, Latte itself recognizes that the filter is contextual: - -```php -use Latte\Runtime\FilterInfo; - -$latte->addFilter('foo', function (FilterInfo $info, string $str): string { - // ... -}); -``` - -Context filters can detect and change the content-type they receive in the `$info->contentType` variable. If the filter is called classically over a variable (e.g. `{$var|foo}`), the `$info->contentType` will contain null. - -The filter should first check if the content-type of the input string is supported. It can also change it. Example of a filter that accepts text (or null) and returns HTML: - -```php -use Latte\Runtime\FilterInfo; - -$latte->addFilter('money', function (FilterInfo $info, float $amount): string { - // first we check if the input's content-type is text - if (!in_array($info->contentType, [null, ContentType::Text])) { - throw new Exception("Filter |money used in incompatible content type $info->contentType."); - } - - // change content-type to HTML - $info->contentType = ContentType::Html; - return "$num Kč"; -}); -``` - -.[note] -In this case, the filter must ensure correct escaping of the data. - -All filters that are used over [blocks|tags#block] (e.g. as `{block|foo}...{/block}`) must be contextual. - - -Functions .{data-version:2.6} -============================= - -By default, all native PHP functions can be used in Latte, unless the sandbox disables it. But you can also define your own functions. They can override the native functions. - -Create a function by registering its name and any PHP callable: - -```php -$latte = new Latte\Engine; -$latte->addFunction('random', function (...$args) { - return $args[array_rand($args)]; -}); -``` - -The usage is then the same as when calling the PHP function: - -```latte -{random(apple, orange, lemon)} // prints for example: apple -``` - - -Functions Using the Class -------------------------- - -The second way to define a function is to [use class|develop#Parameters as a class]. We create a method with the `TemplateFunction` attribute: - -```php -class TemplateParameters -{ - public function __construct( - // parameters - ) {} - - #[Latte\Attributes\TemplateFunction] - public function random(...$args) - { - return $args[array_rand($args)]; - } -} - -$params = new TemplateParameters(/* ... */); -$latte->render('template.latte', $params); -``` - -If you are using PHP 7.x and Latte 2.x, use the `/** @function */` annotation instead of the attribute. - - -Loaders -======= - -Loaders are responsible for loading templates from a source, such as a file system. They are set using the `setLoader()` method: - -```php -$latte->setLoader(new MyLoader); -``` - -The built-in loaders are: - - -FileLoader ----------- - -Default loader. Loads templates from the filesystem. - -Access to files can be restricted by setting the base directory: - -```php -$latte->setLoader(new Latte\Loaders\FileLoader($templateDir)); -$latte->render('test.latte'); -``` - - -StringLoader ------------- - -Loads templates from strings. This loader is very useful for unit testing. It can also be used for small projects where it may make sense to store all templates in a single PHP file. - -```php -$latte->setLoader(new Latte\Loaders\StringLoader([ - 'main.file' => '{include other.file}', - 'other.file' => '{if true} {$var} {/if}', -])); - -$latte->render('main.file'); -``` - -Simplified use: - -```php -$template = '{if true} {$var} {/if}'; -$latte->setLoader(new Latte\Loaders\StringLoader); -$latte->render($template); -``` - - -Creating a Custom Loader ------------------------- - -Loader is a class that implements the [api:Latte\Loader] interface. - - -Tags -==== - -One of the most interesting features of the templating engine is the ability to define new language constructs using tags. It's also a more complex functionality and you need to understand how Latte internally works. - -In most cases, however, the tag is not needed: -- if it should generate some output, use [function|#functions] instead -- if it was to modify some input and return it, use [filter|#filters] instead -- if it was to edit a area of text, wrap it with a [`{block}`|tags#block] tag and use a [filter|#Contextual Filters] -- if it was not supposed to output anything but just call a function, call it with [`{do}`|tags#do] - -If you still want to create a tag, great! All the essentials can be found in [Creating an Extension|creating-extension]. - - -Compiler Passes .{data-version:3.0} -=================================== - -Compiler passes are functions that modify ASTs or collect information in them. In Latte, for example, a sandbox is implemented in this way: it traverses all the nodes of an AST, finds function and method calls, and replaces them with controlled calls. - -As with tags, this is more complex functionality and you need to understand how Latte works under the hood. All the essentials can be found in the [Creating an Extension|creating-extension] chapter. diff --git a/latte/en/filters.texy b/latte/en/filters.texy deleted file mode 100644 index 703bb0ddb3..0000000000 --- a/latte/en/filters.texy +++ /dev/null @@ -1,715 +0,0 @@ -Latte Filters -************* - -.[perex] -Filters are functions that change or format the data to a form we want. This is summary of the built-in filters which are available. - -.[table-latte-filters] -|## String / array transformation -| `batch` | [listing linear data in a table |#batch] -| `breakLines` | [Inserts HTML line breaks before all newlines |#breakLines] -| `bytes` | [formats size in bytes |#bytes] -| `clamp` | [clamps value to the range |#clamp] -| `dataStream` | [Data URI protocol conversion |#datastream] -| `date` | [formats date |#date] -| `explode` | [splits a string by the given delimiter |#explode] -| `first` | [returns first element of array or character of string |#first] -| `implode` | [joins an array to a string |#implode] -| `indent` | [indents the text from left with number of tabs |#indent] -| `join` | [joins an array to a string |#implode] -| `last` | [returns last element of array or character of string |#last] -| `length` | [returns length of a string or array |#length] -| `number` | [formats number |#number] -| `padLeft` | [completes the string to given length from left |#padLeft] -| `padRight` | [completes the string to given length from right |#padRight] -| `random` | [returns random element of array or character of string |#random] -| `repeat` | [repeats the string |#repeat] -| `replace` | [replaces all occurrences of the search string with the replacement |#replace] -| `replaceRE` | [replaces all occurrences according to regular expression |#replaceRE] -| `reverse` | [reverses an UTF‑8 string or array |#reverse] -| `slice` | [extracts a slice of an array or a string |#slice] -| `sort` | [sorts an array |#sort] -| `spaceless` | [removes whitespace |#spaceless], similar to [spaceless |tags] tag -| `split` | [splits a string by the given delimiter |#explode] -| `strip` | [removes whitespace |#spaceless] -| `stripHtml` | [removes HTML tags and converts HTML entities to text |#stripHtml] -| `substr` | [returns part of the string |#substr] -| `trim` | [strips whitespace from the string |#trim] -| `translate` | [translation into other languages |#translate] -| `truncate` | [shortens the length preserving whole words |#truncate] -| `webalize` | [adjusts the UTF‑8 string to the shape used in the URL |#webalize] - -.[table-latte-filters] -|## Letter casing -| `capitalize` | [lower case, the first letter of each word upper case |#capitalize] -| `firstUpper` | [makes the first letter upper case |#firstUpper] -| `lower` | [makes a string lower case |#lower] -| `upper` | [makes a string upper case |#upper] - -.[table-latte-filters] -|## Rounding numbers -| `ceil` | [rounds a number up to a given precision |#ceil] -| `floor` | [rounds a number down to a given precision |#floor] -| `round` | [rounds a number to a given precision |#round] - -.[table-latte-filters] -|## Escaping -| `escapeUrl` | [escapes parameter in URL |#escapeUrl] -| `noescape` | [prints a variable without escaping |#noescape] -| `query` | [generates a query string in the URL |#query] - -There are also escaping filters for HTML (`escapeHtml` and `escapeHtmlComment`), XML (`escapeXml`), JavaScript (`escapeJs`), CSS (`escapeCss`) and iCalendar (`escapeICal`), which Latte uses itself thanks to [context-aware escaping |safety-first#Context-aware escaping] and you do not need to write them. - -.[table-latte-filters] -|## Security -| `checkUrl` | [sanitizes string for use inside href attribute |#checkUrl] -| `nocheck` | [prevents automatic URL sanitization |#nocheck] - -Latte the `src` and `href` attributes [checks automatically |safety-first#link checking], so you almost don't need to use the `checkUrl` filter. - - -.[note] -All built-in filters work with UTF‑8 encoded strings. - - -Usage -===== - -Latte allows calling filters by using the pipe sign notation (preceding space is allowed): - -```latte -

          {$heading|upper}

          -``` - -Filters can be chained, in that case they apply in order from left to right: - -```latte -

          {$heading|lower|capitalize}

          -``` - -Parameters are put after the filter name separated by colon or comma: - -```latte -

          {$heading|truncate:20,''}

          -``` - -Filters can be applied on expression: - -```latte -{var $name = ($title|upper) . ($subtitle|lower)} -``` - -[Custom filters|extending-latte#filters] can be registered this way: - -```php -$latte = new Latte\Engine; -$latte->addFilter('shortify', function (string $s, int $len = 10): string { - return mb_substr($s, 0, $len); -}); -``` - -We use it in a template like this: - -```latte -

          {$text|shortify}

          -

          {$text|shortify:100}

          -``` - - -Filters -======= - - -batch(int length, mixed item): array .[filter]{data-version:2.7} ----------------------------------------------------------------- -Filter that simplifies the listing of linear data in the form of a table. It returns an array of array with the given number of items. If you provide a second parameter this is used to fill up missing items on the last row. - -```latte -{var $items = ['a', 'b', 'c', 'd', 'e']} - -{foreach ($items|batch: 3, 'No item') as $row} - - {foreach $row as $column} - - {/foreach} - -{/foreach} -
          {$column}
          -``` - -Prints: - -```latte - - - - - - - - - - - -
          abc
          deNo item
          -``` - - -breakLines .[filter] --------------------- -Inserts HTML line breaks before all newlines. - -```latte -{var $s = "Text & with \n newline"} -{$s|breakLines} {* outputs "Text & with
          \n newline" *} -``` - - -bytes(int precision = 2) .[filter] ----------------------------------- -Formats a size in bytes to human-readable form. - -```latte -{$size|bytes} 0 B, 1.25 GB, … -{$size|bytes:0} 10 B, 1 GB, … -``` - - -ceil(int precision = 0) .[filter] ---------------------------------- -Rounds a number up to a given precision. - -```latte -{=3.4|ceil} {* outputs 4 *} -{=135.22|ceil:1} {* outputs 135.3 *} -{=135.22|ceil:3} {* outputs 135.22 *} -``` - -See also [#floor], [#round]. - - -capitalize .[filter] --------------------- -Returns a title-cased version of the value. Words will start with uppercase letters, all remaining characters are lowercase. Requires PHP extension `mbstring`. - -```latte -{='i like LATTE'|capitalize} {* outputs 'I Like Latte' *} -``` - -See also [#firstUpper], [#lower], [#upper]. - - -checkUrl .[filter] ------------------- -Enforces URL sanitization. It checks if the variable contains a web URL (ie. HTTP/HTTPS protocol) and prevents the writing of links that may pose a security risk. - -```latte -{var $link = 'javascript:window.close()'} -checked -unchecked -``` - -Prints: - -```latte -checked -unchecked -``` - -See also [#nocheck]. - - -clamp(int|float min, int|float max) .[filter]{data-version:2.9} ---------------------------------------------------------------- -Returns value clamped to the inclusive range of min and max. - -```latte -{$level|clamp: 0, 255} -``` - -Also exists as [function|functions#clamp]. - - -dataStream(string mimetype = detect) .[filter] ----------------------------------------------- -Converts the content to data URI scheme. It can be used to insert images into HTML or CSS without the need to link external files. - -Lets have an image in a variable `$img = Image::fromFile('obrazek.gif')`, then - -```latte - -``` - -Prints for example: - -```latte - -``` - -.[caution] -Requires PHP extension `fileinfo`. - - -date(string format) .[filter] ------------------------------ -Returns a date in the given format using options of [php:strftime] or [php:date] PHP functions. Filter gets a date as a UNIX timestamp, a string or an object of `DateTime` type. - -```latte -{$today|date:'%d.%m.%Y'} -{$today|date:'j. n. Y'} -``` - - -escapeUrl .[filter] -------------------- -Escapes a variable to be used as a parameter in URL. - -```latte -{$name} -``` - -See also [#query]. - - -explode(string separator = '') .[filter]{data-version:2.10.2} -------------------------------------------------------------- -Splits a string by the given delimiter and returns an array of strings. Alias for `split`. - -```latte -{='one,two,three'|explode:','} {* returns ['one', 'two', 'three'] *} -``` - -If the delimiter is an empty string (default value), the input will be divided into individual characters: - -```latte -{='123'|explode} {* returns ['1', '2', '3'] *} -``` - -You can use also alias `split`: - -```latte -{='1,2,3'|split:','} {* returns ['1', '2', '3'] *} -``` - -See also [#implode]. - - -first .[filter]{data-version:2.10.2} ------------------------------------- -Returns the first element of array or character of string: - -```latte -{=[1, 2, 3, 4]|first} {* outputs 1 *} -{='abcd'|first} {* outputs 'a' *} -``` - -See also [#last], [#random]. - - -floor(int precision = 0) .[filter] ----------------------------------- -Rounds a number down to a given precision. - -```latte -{=3.5|floor} {* outputs 3 *} -{=135.79|floor:1} {* outputs 135.7 *} -{=135.79|floor:3} {* outputs 135.79 *} -``` - -See also [#ceil], [#round]. - - -firstUpper .[filter] --------------------- -Converts a first letter of value to uppercase. Requires PHP extension `mbstring`. - -```latte -{='the latte'|firstUpper} {* outputs 'The latte' *} -``` - -See also [#capitalize], [#lower], [#upper]. - - -implode(string glue = '') .[filter] ------------------------------------ -Return a string which is the concatenation of the strings in the array. Alias for `join`. - -```latte -{=[1, 2, 3]|implode} {* outputs '123' *} -{=[1, 2, 3]|implode:'|'} {* outputs '1|2|3' *} -``` - -You can also use an alias `join`: .{data-version:2.10.2} - -```latte -{=[1, 2, 3]|join} {* outputs '123' *} -``` - - -indent(int level = 1, string char = "\t") .[filter] ---------------------------------------------------- -Indents a text from left by a given number of tabs or other characters which we specify in the second optional argument. Blank lines are not indented. - -```latte -
          -{block |indent} -

          Hello

          -{/block} -
          -``` - -Prints: - -```latte -
          -

          Hello

          -
          -``` - - -last .[filter]{data-version:2.10.2} ------------------------------------ -Returns the last element of array or character of string: - -```latte -{=[1, 2, 3, 4]|last} {* outputs 4 *} -{='abcd'|last} {* outputs 'd' *} -``` - -See also [#first], [#random]. - - -length .[filter] ----------------- -Returns length of a string or array. - -- for strings, it will return length in UTF‑8 characters -- for arrays, it will return count of items -- for objects that implement the Countable interface, it will use the return value of the count() -- for objects that implement the IteratorAggregate interface, it will use the return value of the iterator_count() - - -```latte -{if ($users|length) > 10} - ... -{/if} -``` - - -lower .[filter] ---------------- -Converts a value to lowercase. Requires PHP extension `mbstring`. - -```latte -{='LATTE'|lower} {* outputs 'latte' *} -``` - -See also [#capitalize], [#firstUpper], [#upper]. - - -nocheck .[filter] ------------------ -Prevents automatic URL sanitization. Latte [automatically checks|safety-first#Link checking] if the variable contains a web URL (ie. HTTP/HTTPS protocol) and prevents the writing of links that may pose a security risk. - -If the link uses a different scheme, such as `javascript:` or `data:`, and you are sure of its contents, you can disable the check via `|nocheck`. - -```latte -{var $link = 'javascript:window.close()'} - -checked -unchecked -``` - -Prints: - -```latte -checked -unchecked -``` - -See also [#checkUrl]. - - -noescape .[filter] ------------------- -Disables automatic escaping. - -```latte -{var $trustedHtmlString = 'hello'} -Escaped: {$trustedHtmlString} -Unescaped: {$trustedHtmlString|noescape} -``` - -Prints: - -```latte -Escaped: <b>hello</b> -Unescaped: hello -``` - -.[warning] -Misuse of the `noescape` filter can lead to an XSS vulnerability! Never use it unless you are **absolutely sure** what you are doing and that the string you are printing comes from a trusted source. - - -number(int decimals = 0, string decPoint = '.', string thousandsSep = ',') .[filter] ------------------------------------------------------------------------------------- -Formats a number to given number of decimal places. You can also specify a character of the decimal point and thousands separator. - -```latte -{1234.20 |number} 1,234 -{1234.20 |number:1} 1,234.2 -{1234.20 |number:2} 1,234.20 -{1234.20 |number:2, ',', ' '} 1 234,20 -``` - - -padLeft(int length, string pad = ' ') .[filter] ------------------------------------------------ -Pads a string to a certain length with another string from left. - -```latte -{='hello'|padLeft: 10, '123'} {* outputs '12312hello' *} -``` - - -padRight(int length, string pad = ' ') .[filter] ------------------------------------------------- -Pads a string to a certain length with another string from right. - -```latte -{='hello'|padRight: 10, '123'} {* outputs 'hello12312' *} -``` - - -query .[filter]{data-version:2.10} ------------------------------------ -Dynamically generates a query string in the URL: - -```latte -click -search -``` - -Prints: - -```latte -click -search -``` - -Keys with a value of `null` are omitted. - -See also [#escapeUrl]. - - -random .[filter]{data-version:2.10.2} -------------------------------------- -Returns random element of array or character of string: - -```latte -{=[1, 2, 3, 4]|random} {* example output: 3 *} -{='abcd'|random} {* example output: 'b' *} -``` - -See also [#first], [#last]. - - -repeat(int count) .[filter] ---------------------------- -Repeats the string x-times. - -```latte -{='hello'|repeat: 3} {* outputs 'hellohellohello' *} -``` - - -replace(string|array search, string replace = '') .[filter] ------------------------------------------------------------ -Replaces all occurrences of the search string with the replacement string. - -```latte -{='hello world'|replace: 'world', 'friend'} {* outputs 'hello friend' *} -``` - -Multiple replacements can be made at once: .{data-version:2.10.2} - -```latte -{='hello world'|replace: [h => l, l => h]} {* outputs 'lehho worhd' *} -``` - - -replaceRE(string pattern, string replace = '') .[filter] --------------------------------------------------------- -Replaces all occurrences according to regular expression. - -```latte -{='hello world'|replaceRE: '/l.*/', 'l'} {* outputs 'hel' *} -``` - - -reverse .[filter] ------------------ -Reverses given string or array. - -```latte -{var $s = 'Nette'} -{$s|reverse} {* outputs 'etteN' *} -{var $a = ['N', 'e', 't', 't', 'e']} -{$a|reverse} {* returns ['e', 't', 't', 'e', 'N'] *} -``` - - -round(int precision = 0) .[filter] ----------------------------------- -Rounds a number to a given precision. - -```latte -{=3.4|round} {* outputs 3 *} -{=3.5|round} {* outputs 4 *} -{=135.79|round:1} {* outputs 135.8 *} -{=135.79|round:3} {* outputs 135.79 *} -``` - -See also [#ceil], [#floor]. - - -slice(int start, int length = null, bool preserveKeys = false) .[filter]{data-version:2.10.2} ---------------------------------------------------------------------------------------------- -Extracts a slice of an array or a string. - -```latte -{='hello'|slice: 1, 2} {* outputs 'el' *} -{=['a', 'b', 'c']|slice: 1, 2} {* outputs ['b', 'c'] *} -``` - -The slice filter works as the `array_slice` PHP function for arrays and `mb_substr` for strings with a fallback to `iconv_substr` in UTF‑8 mode. - -If the start is non-negative, the sequence will start at that start in the variable. If start is negative, the sequence will start that far from the end of the variable. - -If length is given and is positive, then the sequence will have up to that many elements in it. If the variable is shorter than the length, then only the available variable elements will be present. If length is given and is negative then the sequence will stop that many elements from the end of the variable. If it is omitted, then the sequence will have everything from offset up until the end of the variable. - -Filter will reorder and reset the integer array keys by default. This behaviour can be changed by setting preserveKeys to true. String keys are always preserved, regardless of this parameter. - - -sort .[filter]{data-version:2.9} ---------------------------------- -Filter that sorts an array and maintain index association. - -```latte -{foreach ($names|sort) as $name} - ... -{/foreach} -``` - -Array sorted in reverse order. - -```latte -{foreach ($names|sort|reverse) as $name} - ... -{/foreach} -``` - -You can pass your own comparison function as a parameter: .{data-version:2.10.2} - -```latte -{var $sorted = ($names|sort: fn($a, $b) => $b <=> $a)} -``` - - -spaceless .[filter]{data-version:2.10.2} ------------------------------------------ -Removes unnecessary whitespace from the output. You can also use alias `strip`. - -```latte -{block |spaceless} -
            -
          • Hello
          • -
          -{/block} -``` - -Prints: - -```latte -
          • Hello
          -``` - - -stripHtml .[filter] -------------------- -Converts HTML to plain text. That is, it removes HTML tags and converts HTML entities to text. - -```latte -{='

          one < two

          '|stripHtml} {* outputs 'one < two' *} -``` - -The resulting plain text can naturally contain characters that represent HTML tags, for example `'<p>'|stripHtml` is converted to `

          `. Never output the resulting text with `|noescape`, as this may lead to a security vulnerability. - - -substr(int offset, int length = null) .[filter] ------------------------------------------------ -Extracts a slice of a string. This filter has been replaced by a [#slice] filter. - -```latte -{$string|substr: 1, 2} -``` - - -translate(string message, ...args) .[filter]{data-version:3.0} --------------------------------------------------------------- -It translates expressions into other languages. To make the filter available, you need [set up translator|develop#TranslatorExtension]. You can also use the [tags for translation|tags#Translation]. - -```latte -{='Baskter'|translate} -{$item|translate} -``` - - -trim(string charlist = " \t\n\r\0\x0B\u{A0}") .[filter] -------------------------------------------------------- -Strip leading and trailing characters, by default whitespace. - -```latte -{=' I like Latte. '|trim} {* outputs 'I like Latte.' *} -{=' I like Latte.'|trim: '.'} {* outputs ' I like Latte' *} -``` - - -truncate(int length, string append = '…') .[filter] ---------------------------------------------------- -Shortens a string to the maximum given length but tries to preserve whole words. If the string is truncated it adds ellipsis at the end (this can be changed by the second parameter). - -```latte -{var $title = 'Hello, how are you?'} -{$title|truncate:5} {* Hell… *} -{$title|truncate:17} {* Hello, how are… *} -{$title|truncate:30} {* Hello, how are you? *} -``` - - -upper .[filter] ---------------- -Converts a value to uppercase. Requires PHP extension `mbstring`. - -```latte -{='latte'|upper} {* outputs 'LATTE' *} -``` - -See also [#capitalize], [#firstUpper], [#lower]. - - -webalize .[filter] ------------------- -Converts to ASCII. - -Converts spaces to hyphens. Removes characters that aren’t alphanumerics, underscores, or hyphens. Converts to lowercase. Also strips leading and trailing whitespace. - -```latte -{var $s = 'Our 10. product'} -{$s|webalize} {* outputs 'our-10-product' *} -``` - -.[caution] -Requires package [nette/utils|utils:]. diff --git a/latte/en/functions.texy b/latte/en/functions.texy deleted file mode 100644 index a19a3ea862..0000000000 --- a/latte/en/functions.texy +++ /dev/null @@ -1,126 +0,0 @@ -Latte Functions -*************** - -.[perex] -In addition to the common PHP functions, you can also use these in templates. - -.[table-latte-filters] -| `clamp` | [clamps value to the range |#clamp] -| `divisibleBy`| [checks if a variable is divisible by a number |#divisibleBy] -| `even` | [checks if the given number is even |#even] -| `first` | [returns first element of array or character of string |#first] -| `last` | [returns last element of array or character of string |#last] -| `odd` | [checks if the given number is odd |#odd] -| `slice` | [extracts a slice of an array or a string |#slice] - - -Usage -===== - -Functions are used in the same way as common PHP functions and can be used in all expressions: - -```latte -

          {clamp($num, 1, 100)}

          - -{if odd($num)} ... {/if} -``` - -[Custom functions|extending-latte#functions] can be registered this way: - -```php -$latte = new Latte\Engine; -$latte->addFunction('shortify', function (string $s, int $len = 10): string { - return mb_substr($s, 0, $len); -}); -``` - -We use it in a template like this: - -```latte -

          {shortify($text)}

          -

          {shortify($text, 100)}

          -``` - - -Functions -========= - - -clamp(int|float $value, int|float $min, int|float $max): int|float .[method]{data-version:2.9} ----------------------------------------------------------------------------------------------- -Returns value clamped to the inclusive range of min and max. - -```latte -{=clamp($level, 0, 255)} -``` - -See also [filter clamp|filters#clamp]: - - -divisibleBy(int $value, int $by): bool .[method]{data-version:2.10.2} ---------------------------------------------------------------------- -Checks if a variable is divisible by a number. - -```latte -{if divisibleBy($num, 5)} ... {/if} -``` - - -even(int $value): bool .[method]{data-version:2.10.2} ------------------------------------------------------ -Checks if the given number is even. - -```latte -{if even($num)} ... {/if} -``` - - -first(string|array $value): mixed .[method]{data-version:2.10.2} ----------------------------------------------------------------- -Returns the first element of array or character of string: - -```latte -{=first([1, 2, 3, 4])} {* outputs 1 *} -{=first('abcd')} {* outputs 'a' *} -``` - -See also [#last], [filter first|filters#first]. - - -last(string|array $value): mixed .[method]{data-version:2.10.2} ---------------------------------------------------------------- -Returns the last element of array or character of string: - -```latte -{=last([1, 2, 3, 4])} {* outputs 4 *} -{=last('abcd')} {* outputs 'd' *} -``` - -See also [#first], [filter last|filters#last]. - - -odd(int $value): bool .[method]{data-version:2.10.2} ----------------------------------------------------- -Checks if the given number is odd. - -```latte -{if odd($num)} ... {/if} -``` - - -slice(string|array $value, int $start, int $length=null, bool $preserveKeys=false): string|array .[method]{data-version:2.10.2} -------------------------------------------------------------------------------------------------------------------------------- -Extracts a slice of an array or a string. - -```latte -{=slice('hello', 1, 2)} {* outputs 'el' *} -{=slice(['a', 'b', 'c'], 1, 2)} {* outputs ['b', 'c'] *} -``` - -The slice filter works as the `array_slice` PHP function for arrays and `mb_substr` for strings with a fallback to `iconv_substr` in UTF‑8 mode. - -If the start is non-negative, the sequence will start at that start in the variable. If start is negative, the sequence will start that far from the end of the variable. - -If length is given and is positive, then the sequence will have up to that many elements in it. If the variable is shorter than the length, then only the available variable elements will be present. If length is given and is negative then the sequence will stop that many elements from the end of the variable. If it is omitted, then the sequence will have everything from offset up until the end of the variable. - -Filter will reorder and reset the integer array keys by default. This behaviour can be changed by setting preserveKeys to true. String keys are always preserved, regardless of this parameter. diff --git a/latte/en/guide.texy b/latte/en/guide.texy deleted file mode 100644 index 972e1956f2..0000000000 --- a/latte/en/guide.texy +++ /dev/null @@ -1,41 +0,0 @@ -Getting Started with Latte -************************** - -
          - -Latte is an extraordinary templating system. You will love its syntax. And it is the only PHP templating system with [truly effective protection|safety-first] against critical vulnerabilities. - - -How to Write Templates in Latte? --------------------------------- - -Jazky Latte is ingeniously designed. You will learn it quickly. You just need to know PHP and a few tags. - -- First, familiarize yourself with [Latte syntax|syntax] and [try it all online |https://fiddle.nette.org/latte/] -- Take a look at the [basic set of tags |tags] and [filters] -- Write templates in [editor with Latte support |recipes#Editors and IDE] - - -How to Use Latte in PHP? ------------------------- - -Implementing Latte in your new application is a matter of minutes: - -- First, [install and run Latte |develop#Installation] -- Pamper yourself with the [Tracy debugging tool |develop#Debugging and Tracy] -- Extend Latte with [custom functionality |extending-latte] - - -What Else Can Latte Do? ------------------------ - -The latte comes fully equipped, with all the essentials included. - -- Your productivity will be boosted by the [mechanisms of inheritance |template-inheritance] that reuse repeated elements and structures -- The [Sandbox] armour bunker isolates templates from untrusted sources, such as those edited by users themselves -- For further inspiration, here are [tips and tricks |recipes] - -
          - - -{{description: Latte je nejbezpečnější šablonovací systém pro PHP. Zabraňuje spoustě bezpečnostních zranitelností. Oceníte jeho intuitivní syntaxi a oceníte spoustu užitečných vychytávek.}} diff --git a/latte/en/recipes.texy b/latte/en/recipes.texy deleted file mode 100644 index 240d5b5ffc..0000000000 --- a/latte/en/recipes.texy +++ /dev/null @@ -1,163 +0,0 @@ -Tips and Tricks -*************** - - -Editors and IDE -=============== - -Write templates in an editor or IDE that has support for Latte. It will be much more pleasant. - -- NetBeans IDE has built-in support -- PhpStorm: install the [Latte plugin|https://plugins.jetbrains.com/plugin/7457-latte] in `Settings > Plugins > Marketplace` -- VS Code: search markerplace for "Nette Latte + Neon" plugin -- Sublime Text 3: in Package Control find and install `Nette` package and select Latte in `View > Syntax` -- in old editors use Smarty highlighting for .latte files - -The plugin for PhpStorm is very advanced and can perfectly suggest PHP code. To work optimally, use [typed templates|type-system]. - -[* latte-phpstorm-plugin.webp *] - -Support for Latte can also be found in the web code highlighter [Prism.js|https://prismjs.com/#supported-languages] and editor [Ace|https://ace.c9.io]. - - -Latte Inside JavaScript or CSS -============================== - -Latte can be used very comfortably inside JavaScript or CSS. But how to avoid Latte mistakenly considering JavaScript code or CSS style to be a Latte tag? - -```latte - - - -``` - -**Option 1** - -Avoid situations where a letter immediately follows a `{`, either by inserting a space, line break or quotation mark between them: - -```latte - - - -``` - -**Option 2** - -Completely turn off the processing of Latte tags inside an element using [n:syntax |tags#syntax]: - -```latte - -``` - -**Option 3** - -Switch the Latte tag syntax to double curly braces inside element: - -```latte - -``` - -In JavaScript, [do not put variable in quotes |tags#Printing in JavaScript]. - - -Replacement for `use` Clause -============================ - -How to substitute the `use` clauses used in PHP so that you don't have to write a namespace when accessing a class? PHP example: - -```php -use Pets\Model\Dog; - -if ($dog->status === Dog::STATUS_HUNGRY) { - // ... -} -``` - -**Option 1** - -Instead of clause `use` store the class name in a variable and then instead of `Dog` use `$Dog`: - -```latte -{var $Dog = Pets\Model\Dog::class} - -
          - {if $dog->status === $Dog::STATUS_HUNGRY} - ... - {/if} -
          -``` - -**Option 2** - -If the object `$dog` is an instance of `Pets\Model\Dog`, then `{if $dog->status === $dog::STATUS_HUNGRY}` can be used. - - -Generating XML in Latte -======================= - -Latte can generate any text format (HTML, XML, CSV, iCal, etc.), however, in order to properly escape the displayed data, we must tell it which format we are generating. The [`{contentType}` |tags#contentType] tag is used for this. - -```latte -{contentType application/xml} - -... -``` - -Then, for example, we can generate a sitemap in a similar way: - -```latte -{contentType application/xml} - - - - {$url->loc} - {$url->lastmod->format('Y-m-d')} - {$url->frequency} - {$url->priority} - - -``` - - -Passing Data from an Included Template -====================================== - -The variables that we create with `{var}` or `{default}` in the included template exist only in it and are not available in the including template. -If we want to pass some data from the included template back to the including one, one of the options is to pass an object to the template and set the data to it. - -Main template: - -```latte -{* creates an empty object $vars *} -{var $vars = (object) null} - -{include 'included.latte', vars: $vars} - -{* now contains property foo *} -{$vars->foo} -``` - -Included template `included.latte`: - -```latte -{* write data to the property foo *} -{var $vars->foo = 123} -``` diff --git a/latte/en/safety-first.texy b/latte/en/safety-first.texy deleted file mode 100644 index 83454d8ade..0000000000 --- a/latte/en/safety-first.texy +++ /dev/null @@ -1,371 +0,0 @@ -Latte Is Synonymous with Safety -******************************* - -
          - -Latte is the only PHP templating system with effective protection against the critical Cross-site Scripting (XSS) vulnerability. This is thanks to the so-called context-sensitive escaping. Let's talk, - -- what is the principle of the XSS vulnerability and why is it so dangerous -- what makes Latte so effective in defending against XSS -- why Twig, Blade and other templates can be easily compromised - -
          - - -Cross-Site Scripting (XSS) -========================== - -Cross-site Scripting (XSS for short) is one of the most common vulnerabilities in websites and a very dangerous one at that. It allows an attacker to insert a malicious script (called malware) into a foreign site that executes in the browser of an unsuspecting user. - -What can such a script do? For example, it can send arbitrary content from the compromised site to the attacker, including sensitive data displayed after login. It can modify the page or make other requests on behalf of the user. -For example, if it were webmail, it could read sensitive messages, modify the displayed content, or change settings, e.g., turn on forwarding copies of all messages to the attacker's address to gain access to future emails. - -This is also why XSS tops the list of the most dangerous vulnerabilities. If a vulnerability is discovered on a website, it should be removed as soon as possible to prevent exploitation. - - -How Does the Vulnerability Arise? ---------------------------------- - -The error occurs in the place where the web page is generated and the variables are printed. Imagine that you are creating a search page, and at the beginning there will be a paragraph with the search term in the form: - -```php -echo '

          Search results for ' . $search . '

          '; -``` - -An attacker can write any string, including HTML code like ``, into the search field and thus into the `$search` variable. Since the output is not sanitized in any way, it becomes part of the displayed page: - -```html -

          Search results for

          -``` - -Instead of outputting the search string, the browser executes JavaScript. And thus the attacker takes over the page. - -You might argue that putting code into a variable will indeed execute JavaScript, but only in the attacker's browser. How does it get to the victim? From this perspective, we can distinguish several types of XSS. In our search page example, we are talking about *reflected XSS*. -In this case, the victim needs to be tricked into clicking on a link that contains malicious code in the parameter: - -``` -https://example.com/?search= -``` - -Although it requires some social engineering to make the user to access the link, it's not difficult. Users click on links, whether in emails or on social media, without much thought. And the fact that there's something suspicious in the address can be masked by URL shortener, so the user only sees `bit.ly/xxx`. - -However, there is a second and much more dangerous form of attack known as *stored XSS* or *persistent XSS*, where an attacker manages to store malicious code on the server so that it is automatically inserted into certain pages. - -An example of this is websites where users post comments. An attacker sends a post containing code and it is saved on the server. If the site is not secure enough, it will then run in every visitor's browser. - -It would seem that the point of the attack is to get the ` - - - -

          -``` - -Two ways and two different kinds of escaping data. Within the ` -``` - -However, if we want to insert it into an HTML attribute, we still need to escaping quotes to HTML entities: - -```html -
          -``` - -However, nested context doesn't have to be just JS or CSS. It is also commonly a URL. Parameters in URLs are escaped by converting special characters to sequences starting with `%`. Example: - -``` -https://example.org/?a=Jazz&b=Rock%27n%27Roll -``` - -And when we output this string in an attribute, we still apply escaping according to this context and replace `&` with `&`: - -```html - -``` - -If you've read this far, congratulations, it's been exhausting. Now you have a good idea of what contexts and escaping are. And you don't have to worry about it being complicated. Latte does that for you automatically. - - -Latte vs Naive Systems -====================== - -We've shown how to properly escaping in an HTML document and how crucial it is to know the context, i.e., where you're outputting the data. In other words, how context sensitive escaping works. -While this is a prerequisite for functional XSS defense, **Latte is the only templating system for PHP that does this.** - -How is this possible when all systems today claim to have automatic escaping? -Automatic escaping without knowing the context is a bit of bullshit that **creates a false sense of security**. - -Templating systems like Twig, Laravel Blade and others don't see any HTML structure in the template. Therefore, they don't see contexts either. Compared to Latte, they are blind and naive. They only handle their own markup, everything else is an irrelevant character stream to them: - -
          - -```twig .{file:Twig template as seen by Twig himself} -░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░ -░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░{{ text }}░░░░ -``` - -```twig .{file:Twig template as the designer sees it} -- in text: {{ text }} -- in tag: -- in attribute: -- in unquoted attribute: -- in attribute containing URL: -- in attribute containing JavaScript: -- in attribute containing CSS: -- in JavaScriptu: -- in CSS: -- in comment: -``` - -
          - -Naive systems just mechanically convert `< > & ' "` characters to HTML entities, which is a valid way of escaping in most uses, but far from always. Thus, they cannot detect or prevent various security holes, as we will show below. - -Latte sees the template the same way you do. It understands HTML, XML, recognizes tags, attributes, etc. And because of this, it distinguishes between contexts and treats data accordingly. So it offers really effective protection against the critical Cross-site Scripting vulnerability. - - -Live Demonstration -================== - -On the left you can see the template in Latte, on the right is the generated HTML code. The `$text` variable is output several times, each time in a slightly different context. And therefore escaped a bit differently. You can edit the template code yourself, for example change the content of the variable etc. Try it: - -
          -
          - -``` .{file:template.latte; min-height: 14em}[fiddle-source] -{* TRY TO EDIT THIS TEMPLATE *} -{var $text = "Rock'n'Roll"} -- {$text} -- -- -- -- -- -``` - -
          - -
          - -``` .{file:view-source:...; min-height: 14em}[fiddle-output] -- Rock'n'Roll -- -- -- -- -- -``` - -
          -
          - -Isn't that great! Latte does context-sensitive escaping automatically, so the programmer: - -- doesn't have to think or know how to escape data -- can't be wrong -- can't forget about it - -These aren't even all the contexts that Latte distinguishes when outputting and for which it customizes data treatment. We'll go through more interesting cases now. - - -How to Hack Naive Systems -========================= - -We will use a few practical examples to show how important context differentiation is and why naive templating systems do not provide sufficient protection against XSS, unlike Latte. -We will use Twig as a representative of a naive system in the examples, but the same applies to other systems. - - -Attribute Vulnerability ------------------------ - -Let's try to inject malicious code into the page using the HTML attribute as we [showed above|#How does the vulnerability arise]. Let's have a template in Twig displaying an image: - -```twig .{file:Twig} -{{ -``` - -Note that there are no quotes around the attribute values. The coder may have forgotten them, which just happens. For example, in React, the code is written like this, without quotes, and a coder who is switching languages can easily forget about the quotes. - -The attacker inserts a cleverly constructed string `foo onload=alert('Hacked!')` as the image caption. We already know that Twig can't tell if a variable is being printed in a stream of HTML text, inside an attribute, inside an HTML comment, etc.; in short, it doesn't distinguish between contexts. And it just mechanically converts `< > & ' "` characters to HTML entities. -So the resulting code will look like this: - -```html -foo -``` - -**A security hole has been created!** - -A fake `onload` attribute has become part of the page and the browser executes it immediately after downloading the image. - -Now let's see how Latte handles the same template: - -```latte .{file:Latte} -{$imageAlt} -``` - -Latte sees the template the same way you do. Unlike Twig, it understands HTML and knows that a variable is printed as an attribute value that is not in quotes. That's why it adds them. When an attacker inserts the same caption, the resulting code will look like this: - -```html -foo onload=alert('Hacked!') -``` - -**Latte successfully prevented XSS.** - - -Printing a Variable in JavaScript ---------------------------------- - -Thanks to context-sensitive escaping, it is possible to use PHP variables natively inside JavaScript. - -```latte -

          {$movie}

          - - -``` - -If `$movie` variable stores `'Amarcord & 8 1/2'` string it generates the following output. Notice different escaping used in HTML and JavaScript and also in `onclick` attribute: - -```latte -

          Amarcord & 8 1/2

          - - -``` - - -Link Checking -------------- - -Latte automatically checks whether the variable used in the `src` or `href` attributes contains a web URL (ie protocol HTTP) and prevents the writing of links that may pose a security risk. - -```latte -{var $link = 'javascript:attack()'} - -click here -``` - -Writes: - -```latte -click here -``` - -The check can be turned off using a filter [nocheck|filters#nocheck]. - - -Limits of Latte -=============== - -Latte is not a complete XSS protection for the entire application. We would be unhappy if you stopped to think about security when using Latte. -The goal of Latte is to ensure that an attacker cannot alter the structure of a page, tamper with HTML elements or attributes. But it does not check the content correctness of the data being output. Or the correctness of JavaScript behavior. -That's beyond the scope of the templating system. Verifying the correctness of data, especially those entered by the user and thus untrusted, is an important task for the programmer. diff --git a/latte/en/sandbox.texy b/latte/en/sandbox.texy deleted file mode 100644 index e2f9ad223c..0000000000 --- a/latte/en/sandbox.texy +++ /dev/null @@ -1,54 +0,0 @@ -Sandbox -******* - -.[perex]{data-version:2.8} -Latte has an armored stronghold directly under the hood. It is called sandbox mode and it is an important feature that protects applications that use templates from untrusted sources. For example, when they are edited by the users themselves. - -Sandbox mode makes sure that the sand does not get out of the box. Thus, it provides limited access to tags, filters, functions, methods, etc. -How does it work? We simply define what we want to allow in the template. In the beginning, everything is forbidden and we gradually grant permissions: - -The following code allows the the template to use the `{block}`, `{if}`, `{else}` and `{=}` tags (the latter is a tag for [printing a variable or expression |tags#Printing]) and all filters: - -```php -$policy = new Latte\Sandbox\SecurityPolicy; -$policy->allowTags(['block', 'if', 'else', '=']); -$policy->allowFilters($policy::ALL); - -$latte->setPolicy($policy); -``` - -We can also allow access to global functions, methods or properties of objects: - -```php -$policy->allowFunctions(['trim', 'strlen']); -$policy->allowMethods(Nette\Security\User::class, ['isLoggedIn', 'isAllowed']); -$policy->allowProperties(Nette\Database\Row::class, $policy::ALL); -``` - -Isn't that amazing? You can control everything at a very low level. If the template tries to call an unauthorized function or access an unauthorized method or property, it throws exception `Latte\SecurityViolationException`. - -Creating policies from scratch, when everything is forbidden, may not be convenient, so you can start from a safe foundation: - -```php -$policy = Latte\Sandbox\SecurityPolicy::createSafePolicy(); -``` - -This means that all standard tags are allowed except for `contentType`, `debugbreak`, `dump`, `extends`, `import`, `include`, `layout`, `php`, `sandbox`, `snippet`, `snippetArea`, `templatePrint`, `varPrint`, `widget`. -All standard filters are allowed as well except for `datastream`, `noescape` and `nocheck`. Finally, access to the methods and properties of object `$iterator` is allowed too. - -The rules apply to the template that we insert with the new [`{sandbox}` |tags#Including Templates] tag. Which is a something like `{include}`, but it turns on sandbox mode and also doesn't pass any external variables: - -```latte -{sandbox 'untrusted.latte'} -``` - -Thus, the layout and individual pages can use all tags and variables as before, restrictions will be applied only to the template `untrusted.latte`. - -Some violations, such as the use of a forbidden tag or filter, are detected at compile time. Others, such as calling unallowed methods of an object, at runtime. -The template can also contain any other bugs. In order to prevent an exception from throwing from the sandboxed template, which disrupts the entire rendering, you can define your [own exception handler|develop#exception handler], which, for example, just logs it. - -If we want to turn on sandbox mode directly for all templates, it's easy: - -```php -$latte->setSandboxMode(); -``` diff --git a/latte/en/syntax.texy b/latte/en/syntax.texy deleted file mode 100644 index 535a88ae31..0000000000 --- a/latte/en/syntax.texy +++ /dev/null @@ -1,272 +0,0 @@ -Syntax -****** - -.[perex] -Syntax Latte was born out of the practical requirements of web designers. We were looking for the most user-friendly syntax, with which you can elegantly write constructs that are otherwise a real challenge. -At the same time, all expressions are written exactly the same as in PHP, so you don't have to learn a new language. You just make the most of what you already know. - -Below is a minimal template that illustrates a few basics elements: tags, n:attributes, comments and filters. - -```latte -{* this is a comment *} -
            {* n:if is n:atribut *} -{foreach $items as $item} {* tag representing foreach loop *} -
          • {$item|capitalize}
          • {* tag that prints a variable with a filter *} -{/foreach} {* end of cycle *} -
          -``` - -Let's take a closer look at these important elements and how they can help you build an incredible template. - - -Tags -==== - -A template contains tags that control the template logic (for example, *foreach* loops) or output expressions. For both, a single delimiter `{ ... }` is used, so you don't have to think about which delimiter to use in which situation, as with other systems. -If the `{` character is followed by a quote or space, Latte doesn't consider it to be the beginning of a tag, so you can use JavaScript constructs, JSON, or CSS rules in your templates without any problems. - -See [overview of all tags|tags]. In addition, you can also create [custom tags|extending-latte#tags]. - - -Latte Understands PHP -===================== - -You can use PHP expressions that you know well inside the tags: - -- variables -- strings (including HEREDOC and NOWDOC), arrays, numbers, etc. -- [operators |https://www.php.net/manual/en/language.operators.php] -- function and method calls (which can be restricted by [sandbox]) -- [match |https://www.php.net/manual/en/control-structures.match.php] -- [anonymous functions |https://www.php.net/manual/en/functions.arrow.php] -- [callbacks |https://www.php.net/manual/en/functions.first_class_callable_syntax.php] -- multi-line comments `/* ... */` -- etc... - -In addition, Latte adds several [nice extensions |#Syntactic Sugar] to the PHP syntax. - - -n:attributes -============ - -Each pair tag, such as `{if} … {/if}`, operating upon single HTML element can be written in [n:attribute |#n:attribute] notation. For example, `{foreach}` in the above example could also be written this way: - -```latte -
            -
          • {$item|capitalize}
          • -
          -``` - -The functionality then corresponds to the HTML element in which it is written: - -```latte -{var $items = ['I', '♥', 'Latte']} - -

          {$item}

          -``` - -Prints: - -```latte -

          I

          -

          -

          Latte

          -``` - -By using `inner-` prefix we can alter the behavior so that the functionality applies only to the body of the element: - -```latte -
          -

          {$item}

          -
          -
          -``` - -Prints: - -```latte -
          -

          I

          -
          -

          -
          -

          Latte

          -
          -
          -``` - -Or by using `tag-` prefix the functionality is applied on the HTML tags only: - -```latte -

          Title

          -``` - -Depending on the value of `$url` variable this will print: - -```latte -// when $url is empty -

          Title

          - -// when $url equals 'https://nette.org' -

          Title

          -``` - -However, n:attributes are not only a shortcut for pair tags, there are some pure n:attributes as well, for example the coder's best friend [n:class|tags#n:class]. - - -Filters -======= - -See the summary of [standard filters|filters]. - -Latte allows calling filters by using the pipe sign notation (preceding space is allowed): - -```latte -

          {$heading|upper}

          -``` - -Filters can be chained, in that case they apply in order from left to right: - -```latte -

          {$heading|lower|capitalize}

          -``` - -Parameters are put after the filter name separated by colon or comma: - -```latte -

          {$heading|truncate:20,''}

          -``` - -Filters can be applied on expression: - -```latte -{var $name = ($title|upper) . ($subtitle|lower)} -``` - -On block: - -```latte -

          {block |lower}{$heading}{/block}

          -``` - -Or directly on value (in combination with [`{=expr}`| https://latte.nette.org/en/tags#printing] tag): -```latte -

          {=' Hello world '|trim}

          -``` - - -Comments -======== - -Comments are written this way and do not get into the output: - -```latte -{* this is a comment in Latte *} -``` - -PHP comments work inside tags: - -```latte -{include 'file.info', /* value: 123 */} -``` - - -Syntactic Sugar -=============== - - -Strings Without Quotation Marks -------------------------------- - -Quotation marks can be omitted for simple strings: - -```latte -as in PHP: {var $arr = ['hello', 'btn--default', '€']} - -abbreviated: {var $arr = [hello, btn--default, €]} -``` - -Simple strings are those that are made up purely of letters, digits, underscores and hyphens. They must not begin with a digit and must not begin or end with a hyphen. -It must not be composed of only uppercase letters and underscores, because then it is considered a constant (e.g. `PHP_VERSION`). -And it must not collide with the keywords `and`, `array`, `clone`, `default`, `false`, `in`, `instanceof`, `new`, `null`, `or`, `return`, `true`, `xor`. - - -Short Ternary Operator ----------------------- - -If the third value of the ternary operator is empty, it can be omitted: - -```latte -as in PHP: {$stock ? 'In stock' : ''} - -abbreviated: {$stock ? 'In stock'} -``` - - -Modern Key Notation in the Array --------------------------------- - -Array keys can be written similarly to named parameters when calling functions: - -```latte -as in PHP: {var $arr = ['one' => 'item 1', 'two' => 'item 2']} - -modern: {var $arr = [one: 'item 1', two: 'item 2']} -``` - - -Filters -------- - -Filters can be used for any expression, just enclose the whole in brackets: - -```latte -{var $content = ($text|truncate: 30|upper)} -``` - - -Operator `in` -------------- - -The `in` operator can be used to replace the `in_array()` function. The comparison is always strict: - -```latte -{* like in_array($item, $items, true) *} -{if $item in $items} - ... -{/if} -``` - - -.{data-version:2.9} -Optional Chaining with Undefined-Safe Operator ----------------------------------------------- - -The undefined-safe operator `??->` is similar to the nullsafe operator `?->`, but does not raise an error if a variable, property, or index does not exist at all. - -```latte -{$order??->id} -``` - -this is a way of saying that when `$order` is defined and not null, `$order->id` will be computed, but when `$order` is null or doesn't exist, stop what we’re doing and just return null. - -```latte -{$user??->address??->street} -// roughly means isset($user) && isset($user->address) ? $user->address->street : null -``` - - -A Window into History ---------------------- - -Latte has come up with a number of syntactic candies over the course of its history, which appeared in PHP itself a few years later. For example, in Latte it was possible to write arrays as `[1, 2, 3]` instead of `array(1, 2, 3)` or use the nullsafe operator `$obj?->foo` long before it was possible in PHP itself. Latte also introduced the array expansion operator `(expand) $arr`, which is the equivalent of today's `...$arr` operator from PHP. - - -PHP Limitations in Latte -======================== - -Only PHP expressions can be written in Latte. That is, you cannot declare classes or use [control structures |https://www.php.net/manual/en/language.control-structures.php], such as `if`, `foreach`, `switch`, `return`, `try`, `throw` and others, instead of which Latte offers its [tags]. -You also can't use [attributes |https://www.php.net/manual/en/language.attributes.php], [backticks |https://www.php.net/manual/en/language.operators.execution.php] or [magic constants |https://www.php.net/manual/en/language.constants.magic.php], because that wouldn't make sense. -You can't even use `unset`, `echo`, `include`, `require`, `exit`, `eval`, because they are not functions, but special PHP language constructs, and thus not expressions. - -However, you can work around these limitations by activating the [RawPhpExtension |develop#RawPhpExtension] extension, which allows you to use any PHP code in the `{php ...}` tag at the responsibility of the template author. diff --git a/latte/en/tags.texy b/latte/en/tags.texy deleted file mode 100644 index bd886b2844..0000000000 --- a/latte/en/tags.texy +++ /dev/null @@ -1,1054 +0,0 @@ -Latte Tags -********** - -.[perex] -Summary and description of all Latte built-in tags. - -.[table-latte-tags language-latte] -|## Printing -| `{$var}`, `{...}` or `{=...}` | [prints an escaped variable or expression |#printing] -| `{$var\|filter}` | [prints with filters |#filters] -| `{l}` or `{r}` | prints `{` or `}` character - -.[table-latte-tags language-latte] -|## Conditions -| `{if}` … `{elseif}` … `{else}` … `{/if}` | [condition if|#if-elseif-else] -| `{ifset}` … `{elseifset}` … `{/ifset}` | [condition ifset|#ifset-elseifset] -| `{ifchanged}` … `{/ifchanged}` | [test if there has been a change|#ifchanged] -| `{switch}` `{case}` `{default}` `{/switch}` | [condition switch|#switch-case-default] - -.[table-latte-tags language-latte] -|## Loops -| `{foreach}` … `{/foreach}` | [#foreach] -| `{for}` … `{/for}` | [#for] -| `{while}` … `{/while}` | [#while] -| `{continueIf $cond}` | [continue to next iteration |#continueif-skipif-breakif] -| `{skipIf $cond}` | [skip the current loop iteration |#continueif-skipif-breakif] -| `{breakIf $cond}` | [breaks loop |#continueif-skipif-breakif] -| `{exitIf $cond}` | [early exit |#exitif] -| `{first}` … `{/first}` | [is it the first iteration? |#first-last-sep] -| `{last}` … `{/last}` | [is it the last iteration? |#first-last-sep] -| `{sep}` … `{/sep}` | [will next iteration follow? |#first-last-sep] -| `{iterateWhile}` … `{/iterateWhile}` | [structured foreach|#iterateWhile] -| `$iterator` | [special variable inside foreach loop |#$iterator] - -.[table-latte-tags language-latte] -|## Including other Templates -| `{include 'file.latte'}` | [includes a template from other file |#include] -| `{sandbox 'file.latte'}` | [includes a template in sandbox mode |#sandbox] - -.[table-latte-tags language-latte] -|## Blocks, layouts, template inheritance -| `{block}` | [anonymous block|#block] -| `{block blockname}` | [block definition |template-inheritance#blocks] -| `{define blockname}` | [block defintion for future use |template-inheritance#definitions] -| `{include blockname}` | [prints block |template-inheritance#printing-blocks] -| `{include blockname from 'file.latte'}` | [prints a block from file |template-inheritance#printing-blocks] -| `{import 'file.latte'}` | [loads blocks from another template |template-inheritance#horizontal-reuse] -| `{layout 'file.latte'}` / `{extends}` | [specifies a layout file |template-inheritance#layout-inheritance] -| `{embed}` … `{/embed}` | [loads the template or block and allows you to overwrite the blocks |template-inheritance#unit-inheritance] -| `{ifset blockname}` … `{/ifset}` | [condition if block is defined |template-inheritance#checking-block-existence] - -.[table-latte-tags language-latte] -|## Exception handling -| `{try}` … `{else}` … `{/try}` | [catching exceptions |#try] -| `{rollback}` | [discards try block |#rollback] - -.[table-latte-tags language-latte] -|## Variables -| `{var $foo = value}` | [variable creation |#var-default] -| `{default $foo = value}` | [default value when variable isn't declared |#var-default] -| `{parameters}` | [declares variables, types a default values |#parameters] -| `{capture}` … `{/capture}` | [captures a section to a variable |#capture] - -.[table-latte-tags language-latte] -|## Types -| `{varType}` | [declares type of variable |type-system#varType] -| `{varPrint}` | [suggests types of variables |type-system#varPrint] -| `{templateType}` | [declares types of variables using class |type-system#templateType] -| `{templatePrint}` | [generates class with properties |type-system#templatePrint] - -.[table-latte-tags language-latte] -|## Translation -| `{_string}` | [prints translated |#Translation] -| `{translate}` … `{/translate}` | [translates the content |#Translation] - -.[table-latte-tags language-latte] -|## Others -| `{contentType}` | [switches the escaping mode and sends HTTP header |#contenttype] -| `{debugbreak}` | [sets breakpoint to the code |#debugbreak] -| `{do}` | [evaluates an expression without printing it |#do] -| `{dump}` | [dumps variables to the Tracy Bar |#dump] -| `{spaceless}` … `{/spaceless}` | [removes unnecessary whitespace|#spaceless] -| `{syntax}` | [switches the syntax at runtime |#syntax] -| `{trace}` | [shows stack trace |#trace] - -.[table-latte-tags language-latte] -|## HTML tag helpers -| `n:class` | [smart class attribute |#n:class] -| `n:attr` | [smart HTML attributes |#n:attr] -| `n:tag` | [dynamic name of HTML element |#n:tag] -| `n:ifcontent` | [Omit empty HTML tag |#n:ifcontent] - -.[table-latte-tags language-latte] -|## Available only in Nette Framework -| `n:href` | [link in `` HTML elements |application:creating-links#In the Presenter Template] -| `{link}` | [prints a link |application:creating-links#In the Presenter Template] -| `{plink}` | [prints a link to a presenter |application:creating-links#In the Presenter Template] -| `{control}` | [prints a component |application:components#Rendering] -| `{snippet}` … `{/snippet}` | [a template snippet that can be sent by AJAX |application:ajax#tag-snippet] -| `{snippetArea}` | snippets envelope -| `{cache}` … `{/cache}` | [caches a template section |caching:#caching-in-latte] - -.[table-latte-tags language-latte] -|## Available only with Nette Forms -| `{form}` … `{/form}` | [prints a form element |forms:rendering#latte] -| `{label}` … `{/label}` | [prints a form input label |forms:rendering#label-input] -| `{input}` | [prints a form input element |forms:rendering#label-input] -| `{inputError}` | [prints error message for form input element|forms:rendering#inputError] -| `n:name` | [activates an HTML input element |forms:rendering#n:name] -| `{formPrint}` | [generates Latte form blueprint |forms:rendering#formPrint] -| `{formPrintClass}` | [prints PHP class for form data |forms:in-presenter#mapping-to-classes] -| `{formContext}` … `{/formContext}` | [partial form rendering |forms:rendering#special-cases] - - -Printing -======== - - -`{$var}` `{...}` `{=...}` -------------------------- - -Latte uses the `{=...}` tag to print any expression to the output. If the expression starts with a variable or function call, there is no need to write an equal sign. Which in practice means that it almost never needs to be written: - -```latte -Name: {$name} {$surname}
          -Age: {date('Y') - $birth}
          -``` - -You can write anything you know from PHP as an expression. You just don't have to learn a new language. For example: - - -```latte -{='0' . ($num ?? $num * 3) . ', ' . PHP_VERSION} -``` - -Please don't look for any meaning in the previous example, but if you find one there, write to us :-) - - -Escaping Output ---------------- - -What is the most important task of a template system? To avoid security holes. And that's exactly what Latte does whenever you print something to output. It automatically escapes everything: - -```latte -

          {='one < two'}

          {* prints: '

          one < two

          ' *} -``` - -To be precise, Latte uses context-sensitive escaping, which is such an important and unique feature that we've devoted [a separate chapter to it|safety-first#context-aware-escaping]. - -And if you print HTML-encoded content from a trusted source? Then you can easily turn off escaping: - -```latte -{$trustedHtmlString|noescape} -``` - -.[warning] -Misuse of the `noescape` filter can lead to an XSS vulnerability! Never use it unless you are **absolutely sure** what you are doing and that the string you are printing comes from a trusted source. - - -Printing in JavaScript ----------------------- - -Thanks to context-sensitive escaping, it is wonderfully easy to print variables inside JavaScript, and Latte will properly escape them. - -The variable does not have to be a string, any data type is supported, which is then encoded as JSON: - -```latte -{var $foo = ['hello', true, 1]} - -``` - -Generates: - -```latte - -``` - -This is also the reason why **do not put variable in quotes**: Latte adds them around strings. And if you want to put a string variable into another string, simply concatenate them: - -```latte - -``` - - -Filters -------- - -The printed expression can be modified [by filters|syntax#filters]. For example, this example converts the string to uppercase and shorten it to a maximum of 30 characters: - -```latte -{$string|upper|truncate:30} -``` - -You can also apply filters to parts of an expression as follows: - -```latte -{$left . ($middle|upper) . $right} -``` - - -Conditions -========== - - -`{if}` `{elseif}` `{else}` --------------------------- - -Conditions behave the same way as their PHP counterparts. You can use the same expressions as you know from PHP, you don't have to learn a new language. - -```latte -{if $product->inStock > Stock::MINIMUM} - In stock -{elseif $product->isOnWay()} - On the way -{else} - Not available -{/if} -``` - -Like any pair tag, a pair of `{if} ... {/ if}` can be written as [n:attribute|syntax#n:attributes], for example: - -```latte -

          In stock {$count} items

          -``` - -Do you know that you can add prefix `tag-` to n:attributes? Then the condition will affects only the HTML tags and the content between them will always be printed: - -```latte -
          Hello - -{* prints 'Hello' when $clickable is falsey *} -{* prints 'Hello' when $clickable is truthy *} -``` - -Nice. - - -`{/if $cond}` -------------- - -You may be surprised that the expression in the `{if}` condition can also be specified in the end tag. This is useful in situations where we do not yet know the value of the condition when tag is opened. Let's call it a deferred decision. - -For example, we start listing a table with records from the database, and only after completing the report do we realize that there was no record in the database. So we put condition in the end tag `{/if}` and if there is no record, none of it will be printed: - -```latte -{if} -

          Printing rows from the database

          - - - {foreach $resultSet as $row} - ... - {/foreach} -
          -{/if isset($row)} -``` - -Handy, isn't it? - -You can also use `{else}` in the deferred condition, but not `{elseif}`. - - -`{ifset}` `{elseifset}` ------------------------ - -.[note] -See also [`{ifset block}` |template-inheritance#checking-block-existence] - -Use the `{ifset $var}` condition to determine if a variable (or multiple variables) exists and has a non-null value. It's actually the same as `if (isset($var))` in PHP. Like any pair tag, this can be written in the form of [n:attribute|syntax#n:attributes], so let's show it in example: - -```latte - -``` - - -`{ifchanged}` .{data-version:2.9} ---------------------------------- - -`{ifchanged}` checks if the value of a variable has changed since the last iteration in the loop (foreach, for, or while). - -If we specify one or more variables in the tag, it will check if any of them have changed and prints the contents accordingly. For example, the following example prints the first letter of a name as a heading each time it changes when listing names: - -```latte -{foreach ($names|sort) as $name} - {ifchanged $name[0]}

          {$name[0]}

          {/ifchanged} - -

          {$name}

          -{/foreach} -``` - -However, if no argument is given, the rendered content itself will be checked against its previous state. This means that in the previous example, we can safely omit the argument in the tag. And of course we can also use [n:attribute|syntax#n:attributes]: - -```latte -{foreach ($names|sort) as $name} -

          {$name[0]}

          - -

          {$name}

          -{/foreach} -``` - -You can also include a `{else}` clause inside the `{ifchanged}`. - - -`{switch}` `{case}` `{default}` -------------------------------- -Compares value with multiple options. This is similar to the `switch` structure you know from PHP. However, Latte improves it: - -- uses strict comparison (`===`) -- does not need a `break` - -So it is the exact equivalent of the `match` structure that PHP 8.0 comes with. - -```latte -{switch $transport} - {case train} - By train - {case plane} - By plane - {default} - Differently -{/switch} -``` -.{data-version:2.9} -Clause `{case}` can contain multiple values separated by commas: - -```latte -{switch $status} -{case $status::NEW}new item -{case $status::SOLD, $status::UNKNOWN}not available -{/switch} -``` - - -Loops -===== - -In Latte, all the loops you know from PHP are available to you: foreach, for and while. - - -`{foreach}` ------------ - -You write the cycle exactly the same way as in PHP: - -```latte -{foreach $langs as $code => $lang} - {$lang} -{/foreach} -``` - -In addition, he has some handy tweaks that we will talk about now. - -For example, Latte checks that created variables do not accidentally overwrite global variables of the same name. This will save you when you assume that `$lang` is the current language of the page, and you don't realize that `foreach $langs as $lang` has overwritten that variable. - -The foreach loop can also be written very elegantly and economically with [n:attribute|syntax#n:attributes]: - -```latte -
            -
          • {$item->name}
          • -
          -``` - -Did you know that you can prepend the `inner-` prefix to n:attributes? Now then only the inside part of the element will be repeated in the loop: - -```latte -
          -

          {$item->title}

          -

          {$item->description}

          -
          -``` - -So it prints something like: - -```latte -
          -

          Foo

          -

          Lorem ipsum.

          -

          Bar

          -

          Sit dolor.

          -
          -``` - - -`{else}` .{data-version:2.9}{toc: foreach-else} ------------------------------------------------ - -The `foreach` loop can take an optional `{else}` clause whose text is displayed if the given array is empty: - -```latte -
            - {foreach $people as $person} -
          • {$person->name}
          • - {else} -
          • Sorry, no users in this list
          • - {/foreach} -
          -``` - - -`$iterator` ------------ - -Inside the `foreach` loop the `$iterator` variable is initialized. It holds important information about the current loop. - -- `$iterator->first` - is this the first iteration? -- `$iterator->last` - is this the last iteration? -- `$iterator->counter` - iteration counter, starts from 1 -- `$iterator->counter0` - iteration counter, starts from 0 .{data-version:2.9} -- `$iterator->odd` - is this iteration odd? -- `$iterator->even` - is this iteration even? -- `$iterator->parent` - the iterator surrounding the current one .{data-version:2.9} -- `$iterator->nextValue` - the next item in the loop -- `$iterator->nextKey` - the key of next item in the loop - - -```latte -{foreach $rows as $row} - {if $iterator->first}{/if} - - - - - - - {if $iterator->last}
          {$row->name}{$row->email}
          {/if} -{/foreach} -``` - -The latte is smart and `$iterator->last` works not only for arrays, but also when the loop runs over a general iterator where the number of items is not known in advance. - - -`{first}` `{last}` `{sep}` --------------------------- - -These tags can be used inside the `{foreach}` loop. The contents of `{first}` are rendered for the first pass. -The contents of `{last}` are rendered … can you guess? Yes, for the last pass. These are actually shortcuts for `{if $iterator->first}` and `{if $iterator->last}`. - -The tags can also be written as [n:attributes|syntax#n:attributes]: - -```latte -{foreach $rows as $row} - {first}

          List of names

          {/first} - -

          {$row->name}

          - -
          -{/foreach} -``` - -The contents of the `{sep}` are rendered if the iteration is not the last, so it is suitable for printing delimiters, such as commas between listed items: - -```latte -{foreach $items as $item} {$item} {sep}, {/sep} {/foreach} -``` - -That's pretty practical, isn't it? - - -`{iterateWhile}` .{data-version:2.10} -------------------------------------- - -It simplifies the grouping of linear data during iteration in a foreach loop by performing the iteration in a nested loop as long as the condition is met. [Read instructions in cookbook|cookbook/iteratewhile]. - -It can also elegantly replace `{first}` and `{last}` in the example above: - -```latte -{foreach $rows as $row} - - - {iterateWhile} - - - - - {/iterateWhile true} - -
          {$row->name}{$row->email}
          -{/foreach} -``` - - -`{for}` -------- - -We write the cycle in exactly the same way as in PHP: - -```latte -{for $i = 0; $i < 10; $i++} - Item #{$i} -{/for} -``` - -The tag can also be written as [n:attribute|syntax#n:attributes]: - -```latte -

          {$i}

          -``` - - -`{while}` ---------- - -Again, we write the cycle in exactly the same way as in PHP: - -```latte -{while $row = $result->fetch()} - {$row->title} -{/while} -``` - -Or as [n:attribute|syntax#n:attributes]: - -```latte - - {$row->title} - -``` - -A variant with a condition in the end tag corresponds to the do-while loop in PHP: - -```latte -{while} - {$item->title} -{/while $item = $item->getNext()} -``` - - -`{continueIf}` `{skipIf}` `{breakIf}` -------------------------------------- - -There are special tags you can use to control any loop - `{continueIf ?}` and `{breakIf ?}` which jump to the next iteration and end the loop, respectively, if the conditions are met: - -```latte -{foreach $rows as $row} - {continueIf $row->date < $now} - {breakIf $row->parent === null} - ... -{/foreach} -``` - -.{data-version:2.9} -Tag `{skipIf}` is very similar to `{continueIf}`, but does not increment the counter. So there are no holes in the numbering when you print `$iterator->counter` and skip some items. Also the {else} clause will be rendered when you skip all items. - -```latte -
            - {foreach $people as $person} - {skipIf $person->age < 18} -
          • {$iterator->counter}. {$person->name}
          • - {else} -
          • Sorry, no adult users in this list
          • - {/foreach} -
          -``` - - -`{exitIf}` .{data-version:3.0.5} --------------------------------- - -Ends the rendering of a template or block when a condition is met (i.e. "early exit"). - -```latte -{exitIf !$messages} - -

          Messages

          -
          - {$message} -
          -``` - - -Including Templates -=================== - - -`{include 'file.latte'}` .{toc: include} ----------------------------------------- - -.[note] -See also [`{include block}` |template-inheritance#printing-blocks] - -The `{include}` tag loads and renders the specified template. In our favorite PHP language it's like: - -```php - -``` - -Included templates have not access to the variables of the active context, but have access to the global variables. - -You can pass variables this way: - -```latte -{* since Latte 2.9 *} -{include 'template.latte', foo: 'bar', id: 123} - -{* before Latte 2.9 *} -{include 'template.latte', foo => 'bar', id => 123} -``` - -The template name can be any PHP expression: - -```latte -{include $someVar} -{include $ajax ? 'ajax.latte' : 'not-ajax.latte'} -``` - -The inserted content can be modified using [filters|syntax#filters]. The following example removes all HTML stuff and adjusts the case: - -```latte -{include 'heading.latte' |stripHtml|capitalize} -``` - -The [template inheritance] **is not involved** in this by default. While you can add block tags to templates that are included, they will not replace matching blocks in the template they are included into. Think of includes as independent and shielded parts of pages or modules. This behavior can be changed using the modifier `with blocks` (since Latte 2.9.1): - -```latte -{include 'template.latte' with blocks} -``` - -The relationship between the file name specified in the tag and the file on disk is a matter of [loader|extending-latte#Loaders]. - - -`{sandbox}` .{data-version:2.8} -------------------------------- - -When including a template created by an end user, you should consider sandboxing it (more information in the [sandbox documentation|sandbox]): - -```latte -{sandbox 'untrusted.latte', level: 3, data: $menu} -``` - - -`{block}` -========= - -.[note] -See also [`{block name}` |template-inheritance#blocks] - -Blocks without a name serve to the ability to apply [filters|syntax#filters] to a part of template. For example, you can apply a [strip|filters#strip] filter to remove unnecessary spaces: - -```latte -{block|strip} -
            -
          • Hello World
          • -
          -{/block} -``` - - -Exception Handling -================== - - -`{try}` .{data-version:2.9} ---------------------------- - -This tags makes it extremely easy to build robust templates. - -If an exception occurs while rendering the `{try}` block, the entire block is thrown away and rendering will continue after it: - -```latte -{try} -
            - {foreach $twitter->loadTweets() as $tweet} -
          • {$tweet->text}
          • - {/foreach} -
          -{/try} -``` - -The contents of the optional clause `{else}` are rendered only when an exception occurs: - -```latte -{try} -
            - {foreach $twitter->loadTweets() as $tweet} -
          • {$tweet->text}
          • - {/foreach} -
          - {else} -

          Sorry, the tweets could not be loaded.

          -{/try} -``` - -The tag can also be written as [n:attribute|syntax#n:attributes]: - -```latte -
            - ... -
          -``` - -It is also possible to define [own exception handler|develop#exception handler] for i.e logging: - - -`{rollback}` .{data-version:2.9} --------------------------------- - -The `{try}` block can also be stopped and skipped manually using `{rollback}`. So you do not have to check all the input data in advance, and only during rendering you can decide whether it makes sense to render the object. - -```latte -{try} -
            - {foreach $people as $person} - {skipIf $person->age < 18} -
          • {$person->name}
          • - {else} - {rollback} - {/foreach} -
          -{/try} -``` - - -Variables -========= - - -`{var}` `{default}` -------------------- - -We will create new variables in the template with the `{var}` tag: - -```latte -{var $name = 'John Smith'} -{var $age = 27} - -{* Multiple declaration *} -{var $name = 'John Smith', $age = 27} -``` - -The `{default}` tag works similarly, except that it creates variables only if they do not exist: - -```latte -{default $lang = 'cs'} -``` - -As of Latte 2.7, you can also specify [types of variables|type-system]. For now, they are informative and Latte does not check them. - -```latte -{var string $name = $article->getTitle()} -{default int $id = 0} -``` - - -`{parameters}` .{data-version:2.9} ----------------------------------- - -Just as a function declares its parameters, a template can declare its variables at its beginning: - -```latte -{parameters - $a, - ?int $b, - int|string $c = 10 -} -``` - -Variables `$a` and `$b` without a default value automatically have a default value of `null`. The declared types are still informative and Latte does not check them. - -Other than the declared variables are not passed into the template. This is a difference from the `{default}` tag. - - -`{capture}` ------------ - -By using `{capture}` tag you can capture the output to a variable: - -```latte -{capture $var} -
            -
          • Hello World
          • -
          -{/capture} - -

          Captured: {$var}

          -``` - -The tag can also be written as [n:attribute|syntax#n:attributes]: - -```latte -
            -
          • Hello World
          • -
          -``` - - -Others -====== - - -`{contentType}` ---------------- - -Use the tag to specify what type of content the template represents. The options are: - -- `html` (default type) -- `xml` -- `javascript` -- `css` -- `calendar` (iCal) -- `text` - -Its use is important because it sets [context-sensitive escaping|safety-first#context-aware-escaping] and only then can Latte escape correctly. For example, `{contentType xml}` switches to XML mode, `{contentType text}` turns off escaping completely. - -If the parameter is a full-featured MIME type, such as `application/xml`, it also sends an HTTP header `Content-Type` to the browser: - -```latte -{contentType application/xml} - - - - RSS feed - - ... - - - -``` - - -`{debugbreak}` --------------- - -Specifies the place where code execution will break. It is used for debugging purposes for the programmer to inspect the runtime environment and to ensure the code runs as expected. It supports [Xdebug |https://xdebug.org]. Additionally, you can specify a condition when the code should break. - -```latte -{debugbreak} {* breaks the program *} - -{debugbreak $counter == 1} {* breaks the program if the condition is met *} -``` - - -`{do}` ------- - -Executes the code and does not print anything. - -```latte -{do $num++} -``` - -In Latte 2.7 and earlier, the `{php}` was used. - - -`{dump}` --------- - -Dumps a variable or current context. - -```latte -{dump $name} {* dumps the $name variable *} - -{dump} {* dumps all the defined variables *} -``` - -.[caution] -Requires package [Tracy|tracy:]. - - -`{spaceless}` -------------- - -Removes unnecessary whitespace. It is similar to [spaceless |filters#spaceless] filter. - -```latte -{spaceless} -
            -
          • Hello
          • -
          -{/spaceless} -``` - -Outputs: - -```latte -
          • Hello
          -``` - -The tag can also be written as [n:attribute|syntax#n:attributes]: - - -`{syntax}` ----------- - -Latte tags do not have to be enclosed in single curly braces only. You can choose another separator, even at runtime. This is done by `{syntax…}`, where the parameter can be: - -- double: `{{...}}` -- off: completely disables Latte tags - -By using the n:attribute notation we can disable Latte for a JavaScript block only: - -```latte - -``` - -Latte can be used very comfortably inside JavaScript, just avoid constructs like in this example, where the letter immediately follows `{`, see [Latte inside JavaScript or CSS|recipes#Latte inside JavaScript or CSS]. - -If you turn off Latte with the `{syntax off}` (ie tag, not the n:attribute), it will strictly ignore all tags up to `{/syntax}`. - - -{trace} .{data-version:2.10} ----------------------------- - -Throws an `Latte\RuntimeException` exception, whose stack trace is in the spirit of the templates. Thus, instead of calling functions and methods, it involves calling blocks and inserting templates. If you use a tool for clearly displaying thrown exceptions, such as [Tracy|tracy:], you will clearly see the call stack, including all passed arguments. - - -HTML Tag Helpers -================ - - -n:class -------- - -Thanks to `n:class`, it is very easy to generate the HTML attribute `class` exactly as you need. - -Example: I need the active element to have the `active` class: - -```latte -{foreach $items as $item} - ... -{/foreach} -``` - -And I further need that the first element have the classes `first` and `main`: - -```latte -{foreach $items as $item} - ... -{/foreach} -``` - -And all elements should have the `list-item` class: - -```latte -{foreach $items as $item} - ... -{/foreach} -``` - -Amazingly simple, isn't it? - - -n:attr ------- - -The `n:attr` attribute can generate arbitrary HTML attributes with the same elegance as [n:class|#n:class]. - -```latte -{foreach $data as $item} - -{/foreach} -``` - -Depending on the returned values, it displays eg: - -```latte - - - - - -``` - - -n:tag .{data-version:2.10} --------------------------- - -The `n:tag` attribute can dynamically change the name of an HTML element. - -```latte -

          {$title}

          -``` - -If `$heading === null`, the `

          ` tag is printed without change. Otherwise, the element name is changed to the value of the variable, so for `$heading === 'h3'` it writes: - -```latte -

          ...

          -``` - - -n:ifcontent ------------ - -Prevents an empty HTML element from being printed, ie an element containing nothing but whitespace. - -```latte -
          -
          {$error}
          -
          -``` - -Depending on the values of the variable `$error` this will print: - -```latte -{* $error = '' *} -
          -
          - -{* $error = 'Required' *} -
          -
          Required
          -
          -``` - - -Translation .{data-version:3.0} -=============================== - -To make the translation tags work, you need to [set up translator|develop#TranslatorExtension]. You can also use the [`translate`|filters#translate] filter for translation. - - -`{_...}` --------- - -Translates values into other languages. - -```latte -{_'Basket'} -{_$item} -``` - -Other parameters can also be passed to the translator: - -```latte -{_'Basket', domain: order} -``` - - -`{translate}` -------------- - -Překládá části šablony: - -```latte -

          {translate}Order{/translate}

          - -{translate domain: order}Lorem ipsum ...{/translate} -``` - -The tag can also be written as [n:attribute|syntax#n:attributes], to translate the inside of the element: - -```latte -

          Order

          -``` diff --git a/latte/en/template-inheritance.texy b/latte/en/template-inheritance.texy deleted file mode 100644 index 1068a767bd..0000000000 --- a/latte/en/template-inheritance.texy +++ /dev/null @@ -1,757 +0,0 @@ -Template Inheritance and Reusability -************************************ - -.[perex] -Template reusability and inheritance mechanisms are here to boosts your productivity because each template contains only its unique contents and the repeated elements and structures are reused. We introduce three concepts: [#layout inheritance], [#horizontal reuse] and [#unit inheritance]. - -The concept of Latte template inheritance is similar to PHP class inheritance. You define a **parent template** that other **child templates** can extend from and can override parts of the parent template. It works great when elements share a common structure. Sounds complicated? Don't worry, it's not. - - -Layout Inheritance `{layout}` .{toc: Layout Inheritance} -======================================================== - -Let’s look at layout template inheritance by starting with an example. This is a parent template which we’ll call for example `layout.latte` and it defines an HTML skeleton document. - -```latte - - - - {block title}{/block} - - - -
          - {block content}{/block} -
          - - - -``` - -The `{block}` tags defines three blocks that child templates can fill in. All the block tag does is to tell the template engine that a child template may override those portions of the template by defining their own block of the same name. - -A child template might look like this: - -```latte -{layout 'layout.latte'} - -{block title}My amazing blog{/block} - -{block content} -

          Welcome to my awesome homepage.

          -{/block} -``` - -The `{layout}` tag is the key here. It tells the template engine that this template “extends” another template. When Latte renderes this template, first it locates the parent – in this case, `layout.latte`. - -At that point, the template engine will notice the three block tags in `layout.latte` and replace those blocks with the contents of the child template. Note that since the child template didn’t define the *footer* block, the contents from the parent template is used instead. Content within a `{block}` tag in a parent template is always used as a fallback. - -The output might look like: - -```latte - - - - My amazing blog - - - -
          -

          Welcome to my awesome homepage.

          -
          - - - -``` - -In a child template, blocks can only be located either at the top level or inside another block, ie: - -```latte -{block content} -

          {block title}Welcome to my awesome homepage{/block}

          -{/block} -``` - -Also a block will always be created in regardless of whether the surrounding `{if}` condition is evaluated to be true or false. Contrary to what you might think, this template does define a block. - -```latte -{if false} - {block head} - - {/block} -{/if} -``` - -If you want the output inside block to be displayed conditionally, use the following instead: - -```latte -{block head} - {if $condition} - - {/if} -{/block} -``` - -Data outside of a blocks in a child template are executed before the layout template is rendered, thus you can use it to define variables like `{var $foo = bar}` and propagate data to the whole inheritance chain: - -```latte -{layout 'layout.latte'} -{var $robots = noindex} - -... -``` - - -Multilevel Inheritance ----------------------- -You can use as many levels of inheritance as needed. One common way of using layout inheritance is the following three-level approach: - -1) Create a `layout.latte` template that holds the main look-and-feel of your site. -2) Create a `layout-SECTIONNAME.latte` template for each section of your site. For example, `layout-news.latte`, `layout-blog.latte` etc. These templates all extend `layout.latte` and include section-specific styles/design. -3) Create individual templates for each type of page, such as a news article or blog entry. These templates extend the appropriate section template. - - -Dynamic Layout Inheritance --------------------------- -You can use a variable or any PHP expression as the name of the parent template, so inheritance can behave dynamically: - -```latte -{layout $standalone ? 'minimum.latte' : 'layout.latte'} -``` - -You can also use the Latte API to choose layout template [automatically|develop#automatic-layout-lookup]. - - -Tips ----- -Here are some tips for working with layout inheritance: - -- If you use `{layout}` in a template, it must be the first template tag in that template. - -- Tag `{layout}` has alias `{extends}`. - -- The filename of the extended template depends on the [template loader|extending-latte#Loaders]. - -- You can have as many blocks as you want. Remember, child templates don’t have to define all parent blocks, so you can fill in reasonable defaults in a number of blocks, then only define the ones you need later. - - -Blocks `{block}` .{toc: Blocks} -=============================== - -.[note] -See also anonymous [`{block}` |tags#block] - -A block provides a way to change how a certain part of a template is rendered but it does not interfere in any way with the logic around it. Let’s take the following example to illustrate how a block works and more importantly, how it does not work: - -```latte -{* parent.Latte *} -{foreach $posts as $post} -{block post} -

          {$post->title}

          -

          {$post->body}

          -{/block} -{/foreach} -``` - -If you render this template, the result would be exactly the same with or without the block tags. Blocks have access to variables from outer scopes. It is just a way to make it overridable by a child template: - -```latte -{* child.Latte *} -{layout 'parent.Latte'} - -{block post} -
          -
          {$post->title}
          -
          {$post->text}
          -
          -{/block} -``` - -Now, when rendering the child template, the loop is going to use the block defined in the child template `child.Latte` instead of the one defined in the base one `parent.Latte`; the executed template is then equivalent to the following one: - -```latte -{foreach $posts as $post} -
          -
          {$post->title}
          -
          {$post->text}
          -
          -{/foreach} -``` - -However, if we create a new variable inside a named block or replace a value of existing one, the change will be visible only inside the block: - -```latte -{var $foo = 'foo'} -{block post} - {do $foo = 'new value'} - {var $bar = 'bar'} -{/block} - -foo: {$foo} // prints: foo -bar: {$bar ?? 'not defined'} // prints: not defined -``` - -Contents of block can be modified by [filters|syntax#filters]. The following example removes all HTML and title-cases it: - -```latte -{block title|stripHtml|capitalize}...{/block} -``` - -The tag can also be written as [n:attribute|syntax#n:attributes]: - -```latte -
          - ... -
          -``` - - -Local Blocks .{data-version:2.9} --------------------------------- - -Every block overrides content of parent block of the same name. Except for local blocks. They are something like private methods in class. You can create a template without worrying that – due to coincidence of block names – they would be overwritten by second template. - -```latte -{block local helper} - ... -{/block} -``` - - -Printing Blocks `{include}` .{toc: Printing Blocks} ---------------------------------------------------- - -.[note] -See also [`{include file}` |tags#include] - -To print a block in a specific place, use the `{include blockname}` tag: - -```latte -{block title}{/block} - -

          {include title}

          -``` - -You can also display block from another template: - -```latte -{include footer from 'main.latte'} -``` - -Printed block have not access to the variables of the active context, except if the block is defined in the same file where it is included. However they have access to the global variables. - -You can pass variables this way: - -```latte -{* since Latte 2.9 *} -{include footer, foo: bar, id: 123} - -{* before Latte 2.9 *} -{include footer, foo => bar, id => 123} -``` - -You can use a variable or any expression in PHP as the block name. In this case, add the keyword `block` before the variable, so that it is known at compile-time that it is a block, and not [insert template |tags#include], whose name could also be in the variable: - -```latte -{var $name = footer} -{include block $name} -``` - -Block can also be printed inside itself, which is useful, for example, when rendering a tree structure: - -```latte -{define menu, $items} -
            - {foreach $items as $item} -
          • - {if is_array($item)} - {include menu, $item} - {else} - {$item} - {/if} -
          • - {/foreach} -
          -{/define} -``` - -Instead of `{include menu, ...}` we can also write `{include this, ...}` where `this` means current block. - -Printed content can be modified by [filters|syntax#filters]. The following example removes all HTML and title-cases it: - -```latte -{include heading|stripHtml|capitalize} -``` - - -Parent Block ------------- - -If you need to print the content of the block from the parent template, the `{include parent}` statement will do the trick. This is useful if you want to add to the contents of a parent block instead of completely overriding it. - -```latte -{block footer} - {include parent} - GitHub - Twitter -{/block} -``` - - -Definitions `{define}` .{toc: Definitions} ------------------------------------------- - -In addition to the blocks, there are also "definitions" in Latte. They are comparable with functions in regular programming languages. They are useful to reuse template fragments to not repeat yourself. - -Latte tries to do things easily, so basically the definitions are the same as blocks, and **everything that is said about blocks also applies to definitions**. It differs from blocks in only three ways: - -1) they can accept arguments -2) they can't have [filters|syntax#filters] -3) they are enclosed in tags `{define}` and the content inside these tags is not send to output until you include them. Thanks to that, you can create them anywhere: - -```latte -{block foo}

          Hello

          {/block} -{* prints:

          Hello

          *} - -{define bar}

          World

          {/define} -{* prints nothing *} - -{include bar} -{* prints:

          World

          *} -``` - -Imagine having a generic helper template that defines how to render HTML forms via definitions: - -```latte -{* forms.latte *} -{define input, $name, $value, $type = 'text'} - -{/define} - -{define textarea, $name, $value} - -{/define} -``` - -Arguments of a definitions are always optional with default value `null`, unless default value is specified (here `text` is the default value for `$type`, possible since Latte 2.9.1). As of Latte 2.7, parameter types can also be declared: `{define input, string $name, ...}`. - -Definitions don’t have access to the variables of the active context, but they have access to global variables. - -They are included the [same way as block|#Printing Blocks]: - -```latte -

          {include input, 'password', null, 'password'}

          -

          {include textarea, 'comment'}

          -``` - - -Dynamic Block Names -------------------- - -Latte allows great flexibility in defining blocks because the block name can be any PHP expression. This example defines three blocks named `hi-Peter`, `hi-John` and `hi-Mary`: - -```latte -{* parent.latte *} -{foreach [Peter, John, Mary] as $name} - {block "hi-$name"}Hi, I am {$name}.{/block} -{/foreach} -``` - -For example, we can redefine only one block in a child template: - -```latte -{* child.latte *} -{block hi-John}Hello. I am {$name}.{/block} -``` - -So the output will look like this: - -```latte -Hi, I am Peter. -Hello. I am John. -Hi, I am Mary. -``` - - -Checking Block Existence `{ifset}` .{toc: Checking Block Existence} -------------------------------------------------------------------- - -.[note] -See also [`{ifset $var}` |tags#ifset-elseifset] - -Use the `{ifset blockname}` test to check if a block (or more blocks) exists in the current context: - -```latte -{ifset footer} - ... -{/ifset} - -{ifset footer, header, main} - ... -{/ifset} -``` - -You can use a variable or any expression in PHP as the block name. In this case, add the keyword `block` before the variable to make it clear that it is not the [variable|tags#ifset-elseifset] that is checked: - -```latte -{ifset block $name} - ... -{/ifset} -``` - - -Tips ----- -Here are some tips for working with blocks: - -- The last top-level block does not need to have closing tag (block ends with the end of the document). This simplifies the writing of child templates, which one primary block. - -- For extra readability, you can optionally give a name to your `{/block}` tag, for example `{/block footer}`. However, the name must match the block name. In larger templates, this technique helps you see which block tags are being closed. - -- You can’t directly define multiple block tags with the same name in the same template. But this can be achieved using [#dynamic block names]. - -- You can use [n:attributes|syntax#n:attributes] to define blocks like `

          Welcome to my awesome homepage

          ` - -- Blocks can also be used without names only to apply the [filters |syntax#filters] to the output: `{block|strip} hello {/block}` - - -Horizontal Reuse `{import}` .{toc: Horizontal Reuse} -==================================================== - -The horizontal reuse is a third reusability and inheritance mechanism in Latte. It allows you to load blocks from other templates. It's similar to creating a PHP file with helper functions or a trait. - -Although layout template inheritance is one of the most powerful features of Latte, it is limited to single inheritance; a template can only extend one other template. This limitation makes template inheritance simple to understand and easy to debug: - -```latte -{layout 'layout.latte'} - -{block title}...{/block} -{block content}...{/block} -``` - -Horizontal reuse is a way to achieve the same goal as multiple inheritance, but without the associated complexity: - -```latte -{layout 'layout.latte'} - -{import 'blocks.latte'} - -{block title}...{/block} -{block content}...{/block} -``` - -The `{import}` statement tells Latte to import all blocks and [#definitions] defined in `blocks.latte` into the current template. - -```latte -{* blocks.latte *} - -{block sidebar}...{/block} -``` - -In this example, the `{import}` statement imports the `sidebar` block into the main template. - -The imported template must not [extend|#Layout Inheritance] another template, and its body should be empty. However, the imported template can import other templates. - -The `{import}` tag should be the first template tag after `{layout}`. The template name can be any PHP expression: - -```latte -{import $ajax ? 'ajax.latte' : 'not-ajax.latte'} -``` - -You can use as many `{import}` statements as you want in any given template. If two imported templates define the same block, the first one wins. However, the highest priority is given to the main template, which can overwrite any imported block. - -All overridden blocks can be included gradually by inserting them as [#parent block]: - -```latte -{layout 'base.latte'} - -{import 'blocks.latte'} - -{block sidebar} - {include parent} -{/block} - -{block title}...{/block} -{block content}...{/block} -``` - -In this example, `{include parent}` will correctly call the `sidebar` block from the `blocks.latte` template. - - -Unit Inheritance `{embed}` .{toc: Unit Inheritance}{data-version:2.9} -===================================================================== - -The unit inheritance takes the idea of layout inheritance to the level of content fragments. While layout inheritance works with “document skeletons”, which are brought to life by child templates, the unit inheritance allows you to create skeletons for smaller units of content and reuse them anywhere you like. - -In unit inheritance the `{embed}` tag is the key. It combines the behavior of `{include}` and `{layout}`. It allows you to include another template’s or block's contents and optionally pass variables, just like `{include}` does. It also allows you to override any block defined inside the included template, like `{layout}` does. - -For example we are going to use the collapsible accordion element. Let’s take a look at the element skeleton in template `collapsible.latte`: - -```latte -
          -

          - {block title}{/block} -

          - -
          - {block content}{/block} -
          -
          -``` - -The `{block}` tags defines two blocks that child templates can fill in. Yes, like in the case of parent template in the layout inheritance template. You also see `$modifierClass` variable. - -Let's use our element in template. This is where `{embed}` comes in. It’s a super powerful piece of kit that lets us do all the things: include element's template contents, add variables to it, and add blocks with custom HTML to it: - -```latte -{embed 'collapsible.latte', modifierClass: my-style} - {block title} - Hello World - {/block} - - {block content} -

          Lorem ipsum dolor sit amet, consectetuer adipiscing - elit. Nunc dapibus tortor vel mi dapibus sollicitudin.

          - {/block} -{/embed} -``` - -The output might look like: - -```latte -
          -

          - Hello World -

          - -
          -

          Lorem ipsum dolor sit amet, consectetuer adipiscing - elit. Nunc dapibus tortor vel mi dapibus sollicitudin.

          -
          -
          -``` - -Blocks inside embed tags form a separate layer independent of other blocks. Therefore, they can have the same name as the block outside the embed and are not affected in any way. Using the tag [include|#Printing Blocks] inside `{embed}` tags you can insert blocks here created, blocks from embedded template (which *are not* [local|#Local Blocks]), and also blocks from main template which *are* local. You can also [import blocks|#Horizontal Reuse] from other files: - -```latte -{block outer}…{/block} -{block local hello}…{/block} - -{embed 'collapsible.latte', modifierClass: my-style} - {import 'blocks.latte'} - - {block inner}…{/block} - - {block title} - {include inner} {* works, block is defined inside embed *} - {include hello} {* works, block is local in this template *} - {include content} {* works, block is defined in embedded template *} - {include aBlockDefinedInImportedTemplate} {* works *} - {include outer} {* does not work! - block is in outer layer *} - {/block} -{/embed} -``` - -Embeded templates have not access to the variables of the active context, but they have access to the global variables. - -With `{embed}` you can insert not only templates but also other blocks, so the previous example could be written like this: .{data-version:2.10} - -```latte -{define collapsible} -
          -

          - {block title}{/block} -

          - ... -
          -{/define} - - -{embed collapsible, modifierClass: my-style} - {block title} - Hello World - {/block} - ... -{/embed} -``` - -If we pass an expression to `{embed}` and it is not clear whether it is a block or file name, add the keyword `block` or `file`: - -```latte -{embed block $name} ... {/embed} -``` - - -Use Cases -========= - -There are various types of inheritance and code reuse in Latte. Let's summarize the main concepts for more clearance: - - -`{include template}` --------------------- - -**Use Case:** Using `header.latte` & `footer.latte` inside `layout.latte`. - -`header.latte` - -```latte - -``` - -`footer.latte` - -```latte -
          -
          Copyright
          -
          -``` - -`layout.latte` - -```latte -{include 'header.latte'} - -
          {block main}{/block}
          - -{include 'footer.latte'} -``` - - -`{layout}` ----------- - -**Use Case**: Extending `layout.latte` inside `homepage.latte` & `about.latte`. - -`layout.latte` - -```latte -{include 'header.latte'} - -
          {block main}{/block}
          - -{include 'footer.latte'} -``` - -`homepage.latte` - -```latte -{layout 'layout.latte'} - -{block main} -

          Homepage

          -{/block} -``` - -`about.latte` - -```latte -{layout 'layout.latte'} - -{block main} -

          About page

          -{/block} -``` - - -`{import}` ----------- - -**Use Case**: `sidebar.latte` in `single.product.latte` & `single.service.latte`. - -`sidebar.latte` - -```latte -{block sidebar}{/block} -``` - -`single.product.latte` - -```latte -{layout 'product.layout.latte'} - -{import 'sidebar.latte'} - -{block main}
          Product page
          {/block} -``` - -`single.service.latte` - -```latte -{layout 'service.layout.latte'} - -{import 'sidebar.latte'} - -{block main}
          Service page
          {/block} -``` - - -`{define}` ----------- - -**Use Case**: A function which gets some variables and outputs some markup. - -`form.latte` - -```latte -{define form-input, $name, $value, $type = 'text'} - -{/define} -``` - -`profile.service.latte` - -```latte -{import 'form.latte'} - -
          -
          {include form-input, username}
          -
          {include form-input, password}
          -
          {include form-input, submit, Submit, submit}
          -
          -``` - - -`{embed}` ---------- - -**Use Case**: Embedding `pagination.latte` in `product.table.latte` & `service.table.latte`. - -`pagination.latte` - -```latte - -``` - -`product.table.latte` - -```latte -{embed 'pagination.latte', min: 1, max: $products->count} - {block first}First Product Page{/block} - {block last}Last Product Page{/block} -{/embed} -``` - -`service.table.latte` - -```latte -{embed 'pagination.latte', min: 1, max: $services->count} - {block first}First Service Page{/block} - {block last}Last Service Page{/block} -{/embed} -``` diff --git a/latte/en/type-system.texy b/latte/en/type-system.texy deleted file mode 100644 index 3a5238b23f..0000000000 --- a/latte/en/type-system.texy +++ /dev/null @@ -1,76 +0,0 @@ -Type System -*********** - -
          - -Type system is main thing for the development of robust applications. Latte brings type support to templates. To knowing what data or object type each variable is allows - -- IDE to correctly autocomplete (see [integration and plugins|recipes#Editors and IDE]) -- static analysis to detect errors - -Two points that significantly improve the quality and convenience of development. - -
          - -.[note] -The declared types are informative and Latte does not check them at this time. - -How to start using types? Create a template class, eg `CatalogTemplateParameters`, representing the passed parameters: - -```php -class CatalogTemplateParameters -{ - public function __construct( - public string $langs, - /** @var ProductEntity[] */ - public array $products, - public Address $address, - ) {} -} - -$latte->render('template.latte', new CatalogTemplateParameters( - address: $userAddress, - lang: $settings->getLanguage(), - products: $entityManager->getRepository('Product')->findAll(), -)); -``` - -Then insert the `{templateType}` tag with the full class name (including the namespace) at the beginning of the template. This defines that there are be variables `$langs` and `$products` in the template including the corresponding types. -You can also specify the types of local variables using tags [`{var}` |tags#var-default], `{varType}` and [`{define}` |template-inheritance#definitions]. - -Now the IDE can correctly autocomplete. - -How to save work? How to write a template class or `{varType}` tags as easily as possible? Get them generated. -That is precisely what pair of tags `{templatePrint}` and `{varPrint}` do. -If you place one of these tags in a template, the code of class or template is displayed instead of the normal rendering. Then simply select and copy the code into your project. - - -`{templateType}` ----------------- -The types of parameters passed to the template are declared using class: - -```latte -{templateType MyApp\CatalogTemplateParameters} -``` - - -`{varType}` ------------ -How to declare types of variables? For this purpose use tag `{varType}` for an existing variable, or [`{var}` |tags#var-default]: - -```latte -{varType Nette\Security\User $user} -{varType string $lang} -``` - - -`{templatePrint}` ------------------ -You can also generate this class using the `{templatePrint}` tag. If you place it at the beginning of the template, the code of class is displayed instead of the normal template. Then simply select and copy the code into your project. - - -`{varPrint}` ------------- -The `{varPrint}` tag saves you time. If you place it in a template, the list of `{varType}` tags is displayed instead of the normal rendering. Then simply select and copy the code into your template. - -The `{varPrint}` lists local variables that are not template parameters. If you want to list all variables, use `{varPrint all}`. diff --git a/latte/files/latte-debugging.webp b/latte/files/latte-debugging.webp deleted file mode 100644 index 6635b4071a..0000000000 Binary files a/latte/files/latte-debugging.webp and /dev/null differ diff --git a/latte/files/latte-phpstorm-plugin.webp b/latte/files/latte-phpstorm-plugin.webp deleted file mode 100644 index b33fcb1291..0000000000 Binary files a/latte/files/latte-phpstorm-plugin.webp and /dev/null differ diff --git a/latte/meta.json b/latte/meta.json deleted file mode 100644 index f4c36a04d1..0000000000 --- a/latte/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "3.0", - "repo": "nette/latte", - "composer": "latte/latte" -} diff --git a/mail/cs/@home.texy b/mail/cs/@home.texy index 0a72598138..6b2ba482cd 100644 --- a/mail/cs/@home.texy +++ b/mail/cs/@home.texy @@ -1,5 +1,5 @@ -Odesílání e-mailů -***************** +Nette Mail +**********
          @@ -74,7 +74,7 @@ E-mail je něco jako pohlednice. Nikdy e-mailem neposílejte hesla ani jiné př Přílohy ------- -Do e-mailu lze samozřejmě vkládat přílohy. Slouží k tomu metoda `addAttachment(string $file, string $content = null, string $contentType = null)`. +Do e-mailu lze samozřejmě vkládat přílohy. Slouží k tomu metoda `addAttachment(string $file, ?string $content = null, ?string $contentType = null)`. ```php // vloží do e-mailu soubor /path/to/example.zip pod názvem example.zip @@ -183,6 +183,9 @@ V šabloně potom vytváříme odkazy tak, jak jsme zvyklí. Všechny odkazy vyt Odkaz ``` +.[warning] +Ve verzi 3.0 mělo rozhraní ještě prefix I, takže název byl `Nette\Application\UI\ITemplateFactory`. + Odeslání e-mailu ================ @@ -252,7 +255,7 @@ DKIM DKIM (DomainKeys Identified Mail) je technologie pro zvýšení důvěryhodnosti e-mailů, která také napomáhá odhalení podvržených zpráv. Odeslaná zpráva je podepsána privátním klíčem domény odesílatele a tento podpis je uložen v hlavičce e-mailu. Server příjemce porovná tento podpis s veřejným klíčem uloženým v DNS záznamech domény. Tím, že podpis odpovídá, je prokázáno, že e-mail skutečně pochází z odesílatelovy domény a že během přenosu zprávy nedošlo k její úpravě. -Podepisování e-mailů můžete maileru nastavit přímo v [konfiguraci|#Konfigurace]. Pokud nepoužíváte dependency injection, používá se tímto způsobem: +Podepisování e-mailů můžete maileru nastavit přímo v [konfiguraci |#Konfigurace]. Pokud nepoužíváte dependency injection, používá se tímto způsobem: ```php $options = [ @@ -285,7 +288,7 @@ mail: username: ... # (string) password: ... # (string) timeout: ... # (int) - encryption: ... # (ssl|tls|null) výchozí je null (má alias 'secure') + encryption: ... # (ssl|tls|null) výchozí je null (ve verzi 3.0 'secure') clientHost: ... # (string) výchozí je $_SERVER['HTTP_HOST'] persistent: ... # (bool) výchozí je false @@ -301,7 +304,7 @@ mail: Pomocí volby `context › ssl › verify_peer: false` lze vypnout ověřování SSL certifikátů. **Důrazně nedoporučujeme** tohle dělat, protože se aplikace stane zranitelnou. Místo toho "přidejte certifikáty do uložiště":https://www.php.net/manual/en/openssl.configuration.php. -Pro zvýšení důvěryhodnosti můžeme e-maily podpisovat pomocí [technologie DKIM |https://blog.nette.org/cs/podepisujte-emaily-pomoci-dkim]: +Pro zvýšení důvěryhodnosti můžeme e-maily podpisovat pomocí [technologie DKIM |https://blog.nette.org/cs/podepisujte-emaily-pomoci-dkim]: (od verze 3.1) ```neon mail: @@ -313,4 +316,12 @@ mail: ``` -{{leftbar: nette:@menu-topics}} +Služby DI +========= + +Tyto služby se přidávají do DI kontejneru: + +| Název | Typ | Popis +|----------------------------------------------------- +| `mail.mailer` | [api:Nette\Mail\Mailer] | [třída odesílající e-maily |#Odeslání e-mailu] +| `mail.signer` | [api:Nette\Mail\Signer] | [DKIM podepisování |#DKIM] diff --git a/mail/cs/@meta.texy b/mail/cs/@meta.texy new file mode 100644 index 0000000000..08edde785b --- /dev/null +++ b/mail/cs/@meta.texy @@ -0,0 +1,2 @@ +{{sitename: Nette Dokumentace}} +{{leftbar: nette:@menu-topics}} diff --git a/mail/en/@home.texy b/mail/en/@home.texy index 24e78da04e..b3e676969d 100644 --- a/mail/en/@home.texy +++ b/mail/en/@home.texy @@ -1,9 +1,9 @@ -Sending Emails -************** +Nette Mail +**********
          -Are you going to send emails such as newsletters or order confirmations? Nette Framework provides the necessary tools with a very nice API. We will show: +Are you planning to send emails, such as newsletters or order confirmations? Nette Framework provides the necessary tools with a very user-friendly API. We will show you: - how to create an email, including attachments - how to send it @@ -15,7 +15,7 @@ Are you going to send emails such as newsletters or order confirmations? Nette F Installation ============ -Download and install the package using [Composer|best-practices:composer]: +Download and install the library using [Composer|best-practices:composer]: ```shell composer require nette/mail @@ -25,7 +25,7 @@ composer require nette/mail Creating Emails =============== -Email is a [api:Nette\Mail\Message] object: +An email is a [api:Nette\Mail\Message] object. Let's create one like this: ```php $mail = new Nette\Mail\Message; @@ -33,12 +33,12 @@ $mail->setFrom('John ') ->addTo('peter@example.com') ->addTo('jack@example.com') ->setSubject('Order Confirmation') - ->setBody("Hello, Your order has been accepted."); + ->setBody("Hello,\nYour order has been accepted."); ``` -All parameters must be encoded in UTF-8. +All specified parameters must be in UTF-8 encoding. -In addition to specifying recipients with the `addTo()` method, you can also specify the recipient of copy with `addCc()`, or the recipient of blind copy with `addBcc()`. All these methods, including `setFrom()`, accepts addressee in three ways: +In addition to specifying recipients with `addTo()`, you can also specify recipients for a copy with `addCc()`, or recipients for a blind copy with `addBcc()`. All these methods, including `setFrom()`, accept the addressee in three ways: ```php $mail->setFrom('john.doe@example.com'); @@ -52,9 +52,9 @@ The body of an email written in HTML is passed using the `setHtmlBody()` method: $mail->setHtmlBody('

          Hello,

          Your order has been accepted.

          '); ``` -You don't have to create a text alternative, Nette will generate it automatically for you. And if the email does not have a subject set, it will be taken from the `` element. +You don't need to create a text alternative; Nette will generate it automatically for you. And if the email doesn't have a subject set, it will try to take it from the `<title>` element. -Images can also be extremely easily inserted into the HTML body of an email. Just pass the path where the images are physically located as the second parameter, and Nette will automatically include them in the email: +Images can also be embedded into the HTML body exceptionally easily. Just pass the path where the images are physically located as the second parameter, and Nette will automatically include them in the email: ```php // automatically adds /path/to/images/background.gif to the email @@ -64,26 +64,27 @@ $mail->setHtmlBody( ); ``` -The image embedding algorithm supports the following patterns: `<img src=...>`, `<body background=...>`, `url(...)` inside the HTML attribute `style` and special syntax `[[...]]`. +The image embedding algorithm searches for these patterns: `<img src=...>`, `<body background=...>`, `url(...)` inside the HTML `style` attribute, and the special syntax `[[...]]`. -Can sending emails be even easier? +Could sending emails be even easier? -Emails are like postcards. Never send passwords or other credentials via email. .[tip] +.[tip] +Emails are like postcards. Never send passwords or other credentials via email. Attachments ----------- -You can, of course, attach attachments to email. Use the `addAttachment(string $file, string $content = null, string $contentType = null)`. +You can, of course, attach files to emails. Use the `addAttachment(string $file, ?string $content = null, ?string $contentType = null)` method for this. ```php -// inserts the file /path/to/example.zip into the email under the name example.zip +// attaches the file /path/to/example.zip to the email with the name example.zip $mail->addAttachment('/path/to/example.zip'); -// inserts the file /path/to/example.zip into the email under the name info.zip +// attaches the file /path/to/example.zip named info.zip $mail->addAttachment('info.zip', file_get_contents('/path/to/example.zip')); -// attaches new example.txt file contents "Hello John!" +// attaches the file example.txt with the content "Hello John!" $mail->addAttachment('example.txt', 'Hello John!'); ``` @@ -91,7 +92,7 @@ $mail->addAttachment('example.txt', 'Hello John!'); Templates --------- -If you send HTML emails, it's a great idea to write them in the [Latte|latte:] template system. How to do it? +If you send HTML emails, writing them in the [Latte|latte:] templating system is a great option. How to do it? ```php $latte = new Latte\Engine; @@ -129,13 +130,13 @@ File `email.latte`: </html> ``` -Nette automatically inserts all images, sets the subject according to the `<title>` element, and generates text alternative for HTML body. +Nette automatically embeds all images, sets the subject based on the `<title>` element, and generates a text alternative for the HTML. -Using in Nette Application +Usage in Nette Application -------------------------- -If you use e-mails together with Nette Application, ie presenters, you may want to create links in templates using the `n:href` attribute or the `{link}` tag. Latte basically does not know them, but it's very easy to add them. Creating links can do object `Nette\Application\LinkGenerator`, which you get by passing it using [dependency injection |dependency-injection:passing-dependencies]. +If you use emails together with Nette Application, i.e., with presenters, you might want to create links in templates using the `n:href` attribute or the `{link}` tag. Latte doesn't know these by default, but it's very easy to add them. The `Nette\Application\LinkGenerator` object can create links, and you can get it by passing it using [dependency injection |dependency-injection:passing-dependencies]: ```php use Nette; @@ -177,46 +178,49 @@ class MailSender } ``` -In the template, link is created like in a normal template. All links create over LinkGenerator are absolute: +In the template, you then create links as you are used to. All links created via LinkGenerator will be absolute. ```latte <a n:href="Presenter:action">Link</a> ``` +.[warning] +In version 3.0, the interface still had the I prefix, so the name was `Nette\Application\UI\ITemplateFactory`. + Sending Emails ============== -Mailer is class responsible for sending emails. It implements the [api:Nette\Mail\Mailer] interface and several ready-made mailers are available which we will introduce. +Mailer is a class responsible for sending emails. It implements the [api:Nette\Mail\Mailer] interface, and several pre-made mailers are available, which we will introduce. -The framework automatically adds a `Nette\Mail\Mailer` service based on [configuration|#Configuring] to the DI container, which you get by passing it using [dependency injection |dependency-injection:passing-dependencies]. +The framework automatically adds a `Nette\Mail\Mailer` service based on the [#configuration] to the DI container, which you get by passing it using [dependency injection |dependency-injection:passing-dependencies]. SendmailMailer -------------- -The default mailer is SendmailMailer which uses PHP function [php:mail]. Example of use: +The default mailer is SendmailMailer, which uses the PHP function [php:mail]. Example usage: ```php $mailer = new Nette\Mail\SendmailMailer; $mailer->send($mail); ``` -If you want to set `returnPath` and the server still overwrites it, use `$mailer->commandArgs = '-fmy@email.com'`. +If you want to set the `returnPath` and your server still overwrites it, use `$mailer->commandArgs = '-fmy@email.com'`. SmtpMailer ---------- -To send mail via the SMTP server, use `SmtpMailer`. +To send mail via an SMTP server, use `SmtpMailer`. ```php -$mailer = new Nette\Mail\SmtpMailer([ - 'host' => 'smtp.gmail.com', - 'username' => 'franta@gmail.com', - 'password' => '*****', - 'secure' => 'ssl', -]); +$mailer = new Nette\Mail\SmtpMailer( + host: 'smtp.gmail.com', + username: 'john@gmail.com', + password: '*****', // your password + encryption: 'ssl', // or 'tls' +); $mailer->send($mail); ``` @@ -232,7 +236,7 @@ If you do not specify `host`, the value from php.ini will be used. The following FallbackMailer -------------- -It does not send email but sends them through a set of mailers. If one mailer fails, it repeats the attempt at the next one. If the last one fails, it starts again from the first one. +This mailer does not send emails directly but mediates sending through a set of mailers. If one mailer fails, it retries with the next one. If the last one fails, it starts again from the first one. ```php $mailer = new Nette\Mail\FallbackMailer([ @@ -243,7 +247,7 @@ $mailer = new Nette\Mail\FallbackMailer([ $mailer->send($mail); ``` -Other parameters in the constructor include the number of repeat and waiting time in milliseconds. +Other parameters in the constructor include the number of retries and the waiting time in milliseconds. DKIM @@ -252,7 +256,7 @@ DKIM DKIM (DomainKeys Identified Mail) is a trustworthy email technology that also helps detect spoofed messages. The sent message is signed with the private key of the sender's domain and this signature is stored in the email header. The recipient's server compares this signature with the public key stored in the domain's DNS records. By matching the signature, it is shown that the email actually originated from the sender's domain and that the message was not modified during the transmission of the message. -You can set up mailer to sign email in [configuration|#Configuring]. If you do not use dependency injection, it is used as follows: +You can set up the mailer to sign emails directly in the [#configuration]. If you do not use dependency injection, it is used as follows: ```php $options = [ @@ -268,12 +272,12 @@ $mailer->send($mail); ``` -Configuring -=========== +Configuration +============= -Overview of configuration options for the Nette Mail. If you are not using the whole framework, but only this library, read [how to load the configuration|bootstrap:]. +Overview of configuration options for Nette Mail. If you are not using the entire framework but only this library, read [how to load the configuration|bootstrap:]. -By default, the mailer `Nette\Mail\SendmailMailer` is used to send emails, which is not further configured. However, we can switch it to `Nette\Mail\SmtpMailer`: +By default, the `Nette\Mail\SendmailMailer` is used for sending emails, which requires no further configuration. However, we can switch it to `Nette\Mail\SmtpMailer`: ```neon mail: @@ -285,32 +289,40 @@ mail: username: ... # (string) password: ... # (string) timeout: ... # (int) - encryption: ... # (ssl|tls|null) defaults to null (has alias 'secure') + encryption: ... # (ssl|tls|null) defaults to null (in version 3.0 'secure') clientHost: ... # (string) defaults to $_SERVER['HTTP_HOST'] persistent: ... # (bool) defaults to false - # context for connecting to the SMTP server, defaults to stream_context_get_default() + # stream context options for the SMTP connection, defaults to stream_context_get_default() context: ssl: # all options at https://www.php.net/manual/en/context.ssl.php allow_self_signed: ... ... - http: # all options at https://www.php.net/manual/en/context.http.php + http: # options list at https://www.php.net/manual/en/context.http.php header: ... ... ``` -You can disable SSL certificate authentication using the `context › ssl › verify_peer: false` option. It is **strongly recommended not to do** this as it will make the application vulnerable. Instead, "add certificates to trust store":https://www.php.net/manual/en/openssl.configuration.php. +You can disable SSL certificate verification using the `context › ssl › verify_peer: false` option. **We strongly recommend against doing this** as it makes the application vulnerable. Instead, "add certificates to the trust store":https://www.php.net/manual/en/openssl.configuration.php. -To increase trustfulness, we can sign emails using [DKIM technology |https://blog.nette.org/en/sign-emails-with-dkim]: +To increase trustfulness, we can sign emails using [DKIM technology |https://blog.nette.org/en/sign-emails-with-dkim]: (since version 3.1) ```neon mail: dkim: - domain: myweb.com - selector: lovenette - privateKey: %appDir%/cert/dkim.priv - passPhrase: ... + domain: myweb.com # your domain + selector: lovenette # DKIM selector + privateKey: %appDir%/cert/dkim.key # path to your private key file + passPhrase: ... # passphrase for the private key, if needed ``` -{{leftbar: nette:@menu-topics}} +DI Services +=========== + +These services are added to the DI container: + +| Name | Type | Description +|----------------------------------------------------- +| `mail.mailer` | [api:Nette\Mail\Mailer] | [email sending class |#Sending Emails] +| `mail.signer` | [api:Nette\Mail\Signer] | [DKIM signing |#DKIM] diff --git a/mail/en/@meta.texy b/mail/en/@meta.texy new file mode 100644 index 0000000000..91205786e5 --- /dev/null +++ b/mail/en/@meta.texy @@ -0,0 +1,2 @@ +{{sitename: Nette Documentation}} +{{leftbar: nette:@menu-topics}} diff --git a/mail/meta.json b/mail/meta.json index 4ae87be38b..a29e491e56 100644 --- a/mail/meta.json +++ b/mail/meta.json @@ -1,5 +1,5 @@ { - "version": "4.0", + "version": "3.x", "repo": "nette/mail", "composer": "nette/mail" } diff --git a/migrations/cs/@home.texy b/migrations/cs/@home.texy deleted file mode 100644 index 9ef31ca6f9..0000000000 --- a/migrations/cs/@home.texy +++ /dev/null @@ -1,29 +0,0 @@ -Přechod na novější verze -************************ - -- [z Nette 3.0 na 3.1 |to-3-1] -- [z Nette 2.4 na 3.0 |to-3-0] -- [z Nette 2.3 na 2.4 |to-2-4] -- [z Nette 2.2 na 2.3 |to-2-3] -- [z Nette 2.1 na 2.2 |to-2-2] -- [z Nette 2.0 na 2.1 |to-2-1] - - -Aktualizujte vždy postupně, takže nikoliv z Nette 2.4 na 3.1, ale nejprve na 3.0 a poté na 3.1. - -Před testováním nebo nasazováním doporučujeme nejprve vypnout hlášení chyb `E_USER_DEPRECATED` a povolit jej, až když vše bude fungovat: - -```php -$configurator->enableTracy(); -error_reporting(~E_USER_DEPRECATED); // všimněte si ~ před E_USER_DEPRECATED -``` - - -Upgrade na nejnovější verze ---------------------------- - -Verze balíčků v souboru `composer.json` zvednete na nejnovější nejsnáze pomocí skriptu [composer-frontline.php|https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff]. Spusťte jej v adresáři, kde je i soubor `composer.json`, pomocí: - -```shell -php composer-frontline.php -``` diff --git a/migrations/cs/@left-menu.texy b/migrations/cs/@left-menu.texy deleted file mode 100644 index 82c0d7e4a7..0000000000 --- a/migrations/cs/@left-menu.texy +++ /dev/null @@ -1,9 +0,0 @@ -Přechod na novější verze -************************ -- [Úvod |@home] -- [Z verze 3.0 na 3.1 |to-3-1] -- [Z verze 2.4 na 3.0 |to-3-0] -- [Z verze 2.3 na 2.4 |to-2-4] -- [Z verze 2.2 na 2.3 |to-2-3] -- [Z verze 2.1 na 2.2 |to-2-2] -- [Z verze 2.0 na 2.1 |to-2-1] diff --git a/migrations/cs/to-2-1.texy b/migrations/cs/to-2-1.texy deleted file mode 100644 index 838fbac0c8..0000000000 --- a/migrations/cs/to-2-1.texy +++ /dev/null @@ -1,271 +0,0 @@ -Přechod na verzi 2.1 -******************** - -.[perex] -Nová verze přináší [#nové vlastnosti] a některé [nekompatibility|#nekompatibility], které je třeba projít a kód otestovat před nasazením nové verze. - - -Nové vlastnosti -=============== - - -Application & Presenter ------------------------ -- Presenter: new method `sendJson()` -- PresenterFactory: configurable mapping Presenter name -> Class name -- Route: new pseudo-variables `%basePath%`, `%tld%` and `%domain%` - - -Caching -------- -- added SQLite storage (`Nette/Caching/Storages/SQLiteStorage`) - - -Database (NDB) --------------- -- complete refactoring, a ton of bug fixes -- Connection: - - lazy connection - - all queries are logged (error queries, transactions, …) - - added onConnect event - - DSN in connection panel -- much better (dibi-like) SQL preprocessor -- Selection, ActiveRow: insert() & update() methods return row instances with refetched data -- Selection: added placeholder support select(), group(), having(), order() methods -- SqlLiteral: added placeholder support -- Selection: - - added: WHERE conditions consider NOT for IN operator - - insert() method returns IRow -- drivers: - - new driver for Sqlsrv - - Sqlite supports multi-inserts - - fixes for PostgreSQL - - -Debugger --------- -- Dumper: colored and clickable dumps in HTML or terminal -- Debugger: full stack trace on fatal errors (requires Xdebug) -- Debugger: method `barDump()` accepts options -- BlueScreen: new property `$collapsePaths` which allows you to configure which paths are collapses in stack trace -- Bar: you can see bar after redirect -- Bar: new method `getPanel()` -- Dumper: possibility to include JS & CSS separately - - -Dependency Injection (DI) -------------------------- -- annotation @inject -- auto-generated factories and accessors via interface -- adding compiler extensions via config file -- auto-detection of sections in config file -- configurable presenters via config file -- Container: new methods `findByType()` and `callInjects()` -- bullet syntax for anonymous services - - -Forms ------ -- setOmitted: excludes value from $form->getValues() result -- implemented full validation scopes -- data-nette-rules attribute is JSON -- Form::getOwnErrors() returns only errors attached to form -- Radiolist::getLabel(..., $key) returns label for single item -- added ChoiceControl, MultiChoiceControl and CheckboxList -- SelectBox and CheckboxList: allowes to disable single items -- UploadControl allowes multiple files upload -- validators `Form::INTEGER`, `NUMERIC` and `FLOAT` converts values to integer or float -- validator `Form::URL` prepends `http://` to value -- `Form::getHttpData($htmlName)` returns data for single field -- supports Twitter Bootstrap 2 & 3 (see examples) -- removed dependency on Environment -- improved toggles -- improved netteForms.js - - -Latte ------ -- supports `<tag attr=$val>` without quotes -- new macro `n:name` for `<form> <input> <select> <textarea>` -- partially rendered radiolists using `{input name:$key}` and `{label name:$key}` -- new modifier `|safeurl` which allowes only http(s), ftp and mailto protocols -- safeurl is automatically used for `href`, `src`, `action` and `formaction` attributes (can be bypassed by `|nosafeurl` modifier) -- new modifier `|noescape` which is preferred over exclamation mark -- `{foreach ...|nointerator}` bypasses creating variable `$iterator` -- new macro `n:ifcontent` -- `{include block}` can be written without hash -- template allows helpers overriding -- native support for empty macros `{macro /}` -- a lot of small improvements -- PhpWriter supports indexed arguments like %1.raw - - -Http ----- -- added new SessionPanel -- Helpers: new method `ipMatch()` -- RequestFactory: new method `setProxy()` -- Url: new methods `getQueryParameter()` and `setQueryParameter()` - - -Utils ------ -- Arrays: new method `isList()` -- Arrays: method `flatten()` supports key preserving -- Strings: new methods `findPrefix()` and `normalizeNewLines()` -- Json: supports pretty output -- Neon: is superset of JSON -- Validators: new method `isType()` -- new utility class `FileSystem` -- new utility class `Callback` - - -Mailing -------- -- SmtpMailer: persistent connection -- SmtpMailer: some methods protected and can be overloaded - - -Others ------- -- minified version is PHAR file -- ObjectMixin: new methods `getMagicMethods`, `getExtensionMethod`, `setExtensionMethod` and `checkType` -- ObjectMixin: magic methods setProperty(), getProperty(), isProperty() and addProperty() by @method -- both `RobotLoader` and `NetteLoader` can be registered before existing autoloaders instead of after -- SafeStream: supports `ftruncate` (requires PHP 5.4+) - - -Nekompatibility -=============== - - -Database (NDB) --------------- - -- `Nette\Database\Connection` již není potomkem `PDO`:https://www.php.net/manual/en/class.pdo.php -- přejmenujte metody `exec()` -> `query()`, `fetchColumn()` -> `fetchField()` a `lastInsertId()` -> `getInsertId()` -- `Nette\Database\Statement` je nyní `Nette\Database\ResultSet` a též už není potomkem `PDOStatement`:https://www.php.net/manual/en/class.pdostatement.php -- přejmenujte metody `rowCount()` -> `getRowCount()` a `columnCount()` -> `getColumnCount()` -- MySQL: removed timezone setting. Use onConnect[] event instead. ("commit":https://github.com/nette/nette/commit/61c9d9f1c254334e82b9388cdc95d3256e6fd71e) - -Používáte Nette Database Table (NDBT), tedy skvělou část NDB, ke které se přistupuje přes `$database->table(...)`? - -- metoda `table()` byl přesunuta z `Connection` do nové třídy `Nette\Database\Context`. Ta obsahuje všechny důležité metody pro práci s databází, takže klidně změňte `Connection` za `Context` a máte hotovo. -- proměnné řádku `ActiveRow` jsou nyní read-only, pro změnu slouží metoda `$row->update(['field' => 'value'])`. Věřte, že dřívější chování mělo tolik úskalí, že jiná cesta nebyla. -- změnila se tzv. backjoin syntaxe z `book_tag:tag.name` na `:book_tag.tag.name` (dvojtečka na začátku) -- místo druhého parametru `$having` v metodě `group()` použijte metodu `having()` -- Selection: removed support for INNER join in where statement ("commit":https://github.com/nette/nette/commit/68314840e2429351d1e37e00c6070a21bdc36744) - -(Pokud jste používali `SelectionFactory` v dev-verzi, změňte ji také na `Context`.) - - -Dependency Injection (DI) -------------------------- - -- třída `Nette\Config\Configurator` -> `Nette\Configurator` -- v konfiguračním souboru se sloučily definice `factories` a `services` do společného `services`. Jen těm, co byly původně factories, přidejte klíč `autowired: false`. -- a zavedl se "odrážkový" zápis anonymních služeb: - -```neon -services: - Jmeno\Tridy: self # dříve, ukázalo se jako matoucí - - - Jmeno\Tridy # nyní -``` - -Pracovat přímo s DI kontejnerem není obvykle dobrý nápad, ale pokud už tak činíte: -- tovární metody volejte jako `$container->createService('nazevsluzby')` namísto `$container->createNazevSluzby()` -- zavrženy jsou všechny výchozí továrny jako `createLatte()`, `createCache()`, `createMail()` a `createBasicForm()` -- a ke službám přistupujte raději přes `$container->getService()` či `getByType()` namísto `$container->nazevSluzby` -- Container: removed property `$classes`, removed parameter `$meta` in method `addService()` -- ServiceDefinition: removed property `$internal` and method `setInternal()` -- ContainerBuilder: method `generateClass()` is deprecated, use `generateClasses()[0]` instead -- ContainerBuilder operates on expanded parameters, removed `Helpers: escape()` -- Configurator: deprecated parameter `productionMode`, use `debugMode` instead -- Configurator: methods `setProductionMode`, `isProductionMode` and `detectProductionMode` are deprecated, use `*Debug*` variants instead -- Container: removed deprecated property `$params`, use `$parameters` instead - -Pokud píšete vlastní rozšíření, vězte, že došlo k přejmenování jmenných prostorů `Nette\Config` -> `Nette\DI` a `Nette\Utils\PhpGenerator` -> `Nette\PhpGenerator`. - -Oproti dev-verzi jsou anotace `@inject` a metody `inject()` automaticky zpracovány jen na presenterech. Na jiných službách je zapnete uvedením klíče `inject: true` v definici. - -Používáte-li ještě stařičký `Environment`, bude po vás vyžadovat nastavenou konstantu `TEMP_DIR`, kvůli výkonu. - - -Application & Presenter ------------------------ -- Presenter nyní zabraňuje, aby vám někdo podstrčil do persistentního parametru pole. Pokud ale pole chcete, uveďte ho jako výchozí hodnotu, -- zavržené jsou metody `getService()` (použijte `getContext()->getService()`), dále `getHttpContext()` a `getApplication()` -- magické `getParameter(null)` -> `getParameters()` -- místo divného `invalidateControl()` lze používat `redrawControl()` -- Application: methods `storeRequest()` and `restoreRequest()` are deprecated, call them on `UI\Presenter` instead -- Application\Routers\Route: foo-parameters are not optional when pattern is missing - - -Latte ------ -- výchozím režimem je HTML (namísto XHTML), což lze přepnout v konfiguraci -- automaticky "ouvozovkuje .(slyšeli jste boží slovo!)" atributy v `<a title={$title}>`, což by nemělo způsobit žádnou komplikaci, ale raději to zmiňuji -- atribut `n:input` se mění na `n:name`, aby šel použít nejen na `<input>`, ale i label, select, form a textarea -- zavržená jsou makra `{attr}` (nahrazuje `n:attr`) a `{assign}` -> `{var}` -- doporučujeme místo vykřičníkového zápisu `{!$var}` přejít na `{$var|noescape}`, je to zřejmější -- pokud jste v dev-verzi používali zkrácený zápis bloků `{#block}`, tak do 2.1 se nedostal, nebyl srozumitelný -- native support for empty macros, use for example `{label foo /}` instead of `{label foo}` - -V Latte je novinka, která v `<a href={$url}>` automaticky kontroluje, zda proměnná `$url` neobsahuje něco jako `javascript:hackniWeb()`. Povolené jsou pouze protokoly http, https, ftp, mailto a pochopitelně relativní cesty a kontroluje i atributy src, action, formaction a také `<object data=...>`. Pokud někde potřebujete vypsat URL bez kontroly, použijte modifikátor `|nosafeurl`. - -A nakonec: drobná změna souvisí s ručním vykreslování checkboxů, ale o tom níže. - - -Formuláře ---------- -Checkboxy a RadioListy se nyní vykreslují v praktičtějším tvaru `<label><input>...</label>` namísto `<label>...</label><input>`. Jako důsledek u Checkbox metoda `getLabel()` či `{label}` nevrací nic a `getControl()` či `{input}` HTML v onom novém tvaru. Pokud ale potřebujete staré chování, přepněte se do tzv. partial renderingu přidáním dvojtečky: `{label nazevprvku:}` a `{input nazevprvku:}`. Easy. - -Makro `{control form}` nyní vždy vypisuje chybové zprávy u jednotlivých prvků a nad formulářem jsou jen ty nepřiřazené. Doporučujeme to tak dělat i při manuálním vykreslování, "třeba takto":https://github.com/nette/sandbox/blob/f1819483da6467af1706fbc6b5679aa2f79aa8d0/app/templates/components/form.latte. - -- `setValue()` u prvků kontroluje hodnotu a v případě chyby vyhodí výjimku namísto dřívějšího mlčení -- validační pravidla jako `Form::INTEGER`, `NUMERIC` a `FLOAT` převádí hodnotu na integer resp. float -- TextArea: zrušeny výchozí hodnoty atributů `cols` a `rows` (existovaly jen proto, že to HTML4 vyžadovalo) -- prvky označené `setDisabled()` se neobjeví ve `$form->getValues()` (prohlížeč je totiž vůbec neposílá) -- zavrženo `SelectBox::setPrompt(true)`, místo true použijte řetězec -- přejmenováno `MultiSelectBox::getSelectedItem()` -> `getSelectedItems()` -- v HTML atributech `data-nette-rules` se používá JSON, takže nezapomeňte nasadit aktuální `netteForms.js` -- Form: removed deprecated event `$onInvalidSubmit`, use `$onError` instead -- RadioList: calling `getValue(true)` is deprecated, use `getRawValue()` instead - - -Debugger --------- -- `Nette\Diagnostics\Debugger::$blueScreen` -> `Debugger::getBlueScreen()` -- a adekvátně `$bar` -> `getBar()`, `$logger` -> `getLogger()` a `$fireLogger` -> `getFireLogger()` -- zavrženo `Nette\Diagnostics\Debugger::tryError()`, `catchError()` a také `toStringException()`, místo kterého použijte obyčejný `trigger_error()` -- zavrženy interní `Nette\Diagnostics\Helpers::clickableDump()` a `htmlDump()`, které nahrazuje nová třída `Dumper` - - -Mail ----- -- Zavržená metoda `Nette\Mail\Message::send()`, použijte mailer -- Mail\Message: methods `setHtmlBody()` and `setBody()` render template immediately -- MimePart: removed method `generateMessage()`, use `getEncodedMessage()` instead - - -ostatní -------- -- Nette Framework opouští PHP 5.2, s přechodem na jmenné prostory vám pomůže nástroj `migration-53.php` -- minimalizovaná verze se nyní generuje ve "formátu PHAR":https://www.php.net/manual/en/book.phar.php, takže v distribuci místo `nette.min.php` najdete soubor `nette.phar`, se kterým se však pracuje úplně stejně -- `Nette\Utils\Finder::find($mask)` filtruje podle masky nejen soubory, ale i adresáře -- do `Nette\Security\User` se v konstruktoru předává autentikátor, pozor na kruhové závislosti -- v loaderu se už nenastavuje `iconv_set_encoding()` a `mb_internal_encoding()` -- zavrženy konstanty `NETTE, NETTE_DIR a NETTE_VERSION_ID` -- a třída `Nette\Loaders\AutoLoader` -- a proměnná `Nette\Framework::$iAmUsingBadHost` -- doporučujeme přestat používat `callback()` a třídu `Nette\Callback`, neboť globální funkce mohou způsobit "komplikace":https://github.com/nette/nette/issues/1187 -- přejmenoval se jmenný prostor `Nette\Utils\PhpGenerator` -> `Nette\PhpGenerator` -- Nette varuje hláškou „Possible problem: you are sending a cookie while already having some data in output buffer,“ pokud se snažíte odeslat HTTP hlavičku nebo cookie a byl již odeslán nějaký výstup - byť do bufferu. Buffer totiž může přetéct a proto to varování. -- InstanceFilter: removed entirely -- ResursiveFilter: removed method `accept()` and parameter `$childrenCallback` in constructor -- RequestFactory: removed method `setEncoding()`, only UTF-8 and binary (via `setBinary()`) is now supported -- ObjectMixin: removed method `callProperty()` -- ObjectMixin: removes support for non-registered extension methods (`*_prototype_*` functions) - -{{priority: -5}} diff --git a/migrations/cs/to-2-2.texy b/migrations/cs/to-2-2.texy deleted file mode 100644 index a226618867..0000000000 --- a/migrations/cs/to-2-2.texy +++ /dev/null @@ -1,81 +0,0 @@ -Přechod na verzi 2.2 -******************** - -Verze 2.2 přichází s úplně novou infrastrukturou. Původní repozitář "Nette":https://github.com/nette/nette byl rozdělen do nových samostatných komponent. .[perex] - -Nette bylo rozděleno do samostatných komponent "Application":https://github.com/nette/application, "Bootstrap":https://github.com/nette/bootstrap, "Caching":https://github.com/nette/caching, "ComponentModel":https://github.com/nette/component-model, "Nette Database":https://github.com/nette/database, "DI":https://github.com/nette/di, "Finder":https://github.com/nette/finder, "Forms":https://github.com/nette/forms, "Http":https://github.com/nette/http, "Latte":https://github.com/nette/latte, "Mail":https://github.com/nette/mail, "Neon":https://github.com/nette/neon, "PhpGenerator":https://github.com/nette/php-generator, "Reflection":https://github.com/nette/reflection, "RobotLoader":https://github.com/nette/robot-loader, "SafeStream":https://github.com/nette/safe-stream, "Security":https://github.com/nette/security, "Tokenizer":https://github.com/nette/tokenizer, "Tracy":https://github.com/nette/tracy a "Utils":https://github.com/nette/utils. -Každá komponenta má vlastní repozitář, issue tracker a číslování verzí. - -Stále si však můžete stáhnout celý framework nebo jej nainstalovat pomocí [Composeru |best-practices:composer] příkazem `composer require nette/nette`. - - -Tracy ------ - -Laděnka byla přejmenována na `Tracy`, místo názvu `Nette\Diagnostics\Debugger` se tedy používá `Tracy\Debugger`. Původní třída je z důvodu zpětné kompatibility stále funkční. - -Pokud píšete doplňky, prefix názvů CSS tříd se změnil z `nette-` na `tracy-` a třída `nette-toggle-collapsed` na dvojici `tracy-toggle tracy-collapsed`. Původní třídy jsou u starých doplňku změněny na nové automaticky. - - -Latte ------ - -Šablonovací systém byl vždy poměrně úzce provázán s dalšími částmi frameworku, zejména třídami z jmenného prostoru `Nette\Templating`. Aby bylo Latte samostatně použitelné, bylo potřeba mu vymyslet nové snadno použitelné API, které se obejde bez těchto pomocných tříd. A vypadá takto: - -```php -$latte = new Latte\Engine; // nikoliv Nette\Latte\Engine -$latte->setTempDirectory('/path/to/cache'); - -$latte->addFilter('money', function ($val) { return /* ... */; }); // dříve registerHelper() - -$latte->onCompile[] = function ($latte) { - $latte->addMacro(/* ... */); // when you want add some own macros, see http://goo.gl/d5A1u2 -}; - -$latte->render('template.latte', $parameters); -// or $html = $latte->renderToString('template.latte', $parameters); -``` - -Jak vidíte, Latte si řeší samo načítání šablon a jejich kešování, čímž pádem původní `FileTemplate` a vlastně celý `Nette\Templating` z velké míry pozbývá na smyslu existence. Tyto třídy i nadále fungují a snaží se zajistit kompatibilitu s novým Latte, nicméně jsou zavržené. Ze stejného důvodu jsou zavržené i třídy `Nette\Utils\LimitedScope`, `Nette\Caching\Storages\PhpFileStorage` a služba `templateCacheStorage`. - -Naopak Application přináší vlastní třídu `Template` (nahrazující FileTemplate a zajišťují kompatibilitu) a továrnu `TemplateFactory`, která rozšiřuje možnosti, jak v presenterech a komponentách pracovat se šablonami. - - -Ostatní -------- -Třídy `Nette\ArrayHash`, `ArrayList`, `DateTime`, `Image` a `ObjectMixin` jsou nyní součástí balíčku `Utils`, proto i jejich namespace byl změněn z `Nette` na `Nette\Utils`. Obdobně `Nette\Utils\Neon` se stalo součástí balíčku `Neon` a bude mít namespace `Nette\Neon\Neon`. Aby změna byla transparentní, vytváří se pro tyto a některé další třídy tiše aliasy. Aliasy ostatních tříd vytváří Nette Loader spuštěný v `loader.php` a vypisuje přitom varování (abyste mohli svůj kód upravit). - -Zavržena (tj. stále funguje, jen vyhodí E_USER_DEPRECATED) je třída `Nette\Utils\MimeTypeDetector`, která od PHP 5.3 není potřeba, neboť ji plně nahrazuje rozšíření "Fileinfo":https://www.php.net/manual/en/book.fileinfo.php (pod Windows jej nezapomeňte zapnout v php.ini). - -Byla zrušena podpora anotace `@serializationVersion` a dohledávání tříd pro vlastní anotace - tyto věci nebyly známé ani používané, ale měly negativní vliv na výkon. - -A nakonec, chybné odkazy v šabloně nyní začínají hashem, tj. místo `error:...` se vypisuje `#error:`, aby když na takový odkaz omylem kliknete, browser nevypsal strašidelnou hlášku. Upravte si proto "CSS":https://github.com/nette/sandbox/commit/ac7a0fd2d707160426ab184442b8d68c590f8de2#diff-2. - - -Novinky -------- - -Formuláře nyní přenášejí parametr `do` v POST datech, takže nebude strašit v URL. Přibyly nové validátory `Form::MIN` a `Form::MAX`. A do funkcí obsluhující událost `onSuccess` se nyní jako druhý parametr předávají hodnoty formuláře, takže ušetříte psaní `$values = $form->getValues()`. - -V databázi přibyla nová funkce fetchAssoc(). Můžete se podívat na pár "příkladů použití":https://github.com/nette/utils/blob/master/tests/Utils/Arrays.associate().phpt v testech. - -Anotace `@inject` nyní respektují aliasy definované pomocí `use`. - -Pokud do `data-` atributů třídy Html vložíte pole, bude se serializovat do JSONu. - -A přibyla nová třída `Nette\Security\Passwords`, která řeší hashování hesel (vyžaduje minimálně PHP 5.3.7). - -V souboru `config.neon` můžete jednotlivým uživatelů definovat také jejich role: - -```neon -nette: - security: - users: - john: - password: ********** - roles: - - manager - - reporter -``` - -{{priority: -5}} diff --git a/migrations/cs/to-2-3.texy b/migrations/cs/to-2-3.texy deleted file mode 100644 index 2678fce526..0000000000 --- a/migrations/cs/to-2-3.texy +++ /dev/null @@ -1,103 +0,0 @@ -Přechod na verzi 2.3 -******************** - -Verzí Nette 2.3 se rozumí, že máte tyto balíčky nainstalované ve verze 2.3.*. Ostatní balíčky mohou mít vyšší nebo nižší čísla verzí, kompatibilitu hlídá Composer. - -```json -"require": { - "nette/application": "2.3.*", - "nette/bootstrap": "2.3.*", - "nette/caching": "2.3.*", - "nette/database": "2.3.*", - "nette/di": "2.3.*", - "nette/forms": "2.3.*", - "nette/http": "2.3.*", - "nette/security": "2.3.*", -}, -``` - - -Application ------------ -- routes and presenter names are **case sensitive.** Nette will warn you if you use the wrong case in presenter name. But due to performance limitation it is not checking Route mask - you should check them manually. Correct is `<presenter=UpperCasedDefaultValue>` and `<presenter url-cased-regexp-mask>`. -- `Route::addStyle()` & `Route::setStyleProperty()` are deprecated and now will trigger `E_USER_DEPRECATED` -- unsupported template extension `.phtml` and [old link syntax](https://github.com/nette/application/commit/695158d60fa443e1e65c0e1edebc3af9de1d39b6) - - -Bootstrap ---------- -- removed deprecated constants `Configurator::DEVELOPMENT` and `PRODUCTION` -- `Configurator::setDebugMode()` accepts only bool / string / array -- in config file you can move all sections placed in `nette` to one level up. If you move up one of the sections `container`, `mailer` or `debugger`, rename it to `di`, `mail` and `tracy`. - - -Caching -------- -- ancient and deprecated ArrayAccess syntax `$val = $cache[$key]` or `$cache[$key] = $val` triggers `E_USER_DEPRECATED`. Use please `$cache->load($key)` and `$cache->save($key, $val)` - - -Database --------- -- MySqlDriver by default uses utf8mb4 encoding for MySQL >= 5.5.3 instead of utf8 ("see":https://forum.nette.org/en/22055-nette-2-3-0-beta-for-testing#p150705, **maybe it will be reverted**) -- `IReflection` was changed to twins `IStructure` and `IConventions` -- to ensure that new SQL translator do the same job as older one, you can install special tool named `CompatibilityChecker22` - - -DI ---- -- removed support for placing services inside extension section in configuration file -- removed support for dynamically added extensions -- for replacing service dynamically (through removeService, addService calls), newly added service must be instance of same interface/class as original service - - -Finder ------- -* `Finder::filter()` callback always receives as argument (at least) a `FilesystemIterator` - - -Forms ------ -- internal filtering methods like `Nette\Forms\Controls\TextBase::filterFloat` was removed -- internal validation methods like `Nette\Forms\Controls\TextBase::validateFloat` was moved to `Nette\Forms\Validator`, as well as `Rules::$defaultMessages` -- Buttons and Hidden fields are generated without HTML ID. Relying on autogenerated ID is very bad, if you want ID, set it via `setHtmlId()` -- RadioList items are generated without ID too. You can enable it via `$radioList->generateId = true`. But again: set you base ID via `setHtmlId()` -- filters added via `TextBase::addFilter()` are processed during validation. -- now you can add filters to conditions `$input->addCondition(...)->addFilter(...)` - - -Http ----- -- `Request::getUrl()` is immutable - - -Mail ----- -- if you use variable `$mail` in template, you have to pass it to the template manually -- but better than `{var $mail->subject = "Your new order"}` is this `<title>Your new order`, isn't it? -- if you have linked images (with relative paths) in template, pass base file path to images as second parameter to `setHtmlBody()` -- there is no need to cast templates to `(string)` - - -RobotLoader ------------ -- is now case sensitive and will warn you if you use the wrong case in class name - - -SafeStream ----------- -- it is recommended to change protocol `safe://...` to namespaced `nette.safe://...` - - -Tracy ------ -- fasten your belt if you will use Tracy, she is now really fast :-) - - -Utils ------ -- `Image::from()` throws `ImageException` when is unable to decode file -- `Image::getFormatFromString` is deprecated -- `Strings::chr` and `normalize` now works only with UTF-8 encoding -- `Strings::chr()` throws `Nette\InvalidArgumentException` if code point is not in valid range - -{{priority: -5}} diff --git a/migrations/cs/to-2-4.texy b/migrations/cs/to-2-4.texy deleted file mode 100644 index 7e9857cc16..0000000000 --- a/migrations/cs/to-2-4.texy +++ /dev/null @@ -1,124 +0,0 @@ -Přechod na verzi 2.4 -******************** - -Minimální požadovaná verze PHP je 5.6 (pro Latte a Tracy 5.4). - -Verzí Nette 2.4 se rozumí, že máte tyto balíčky nainstalované ve verze 2.4.* (resp. 2.5.*). Ostatní balíčky mohou mít vyšší nebo nižší čísla verzí, kompatibilitu hlídá Composer. - -```json -"require": { - "nette/application": "2.4.*", - "nette/bootstrap": "2.4.*", - "nette/caching": "2.5.*", - "nette/database": "2.4.*", - "nette/di": "2.4.*", - "nette/forms": "2.4.*", - "nette/http": "2.4.*", - "nette/security": "2.4.*", -}, -``` - - -Deprecated -========== - -Před testováním nebo nasazováním doporučujeme nejprve vypnout hlášení chyb `E_USER_DEPRECATED` a povolit jej, až když vše bude fungovat: - -```php -$configurator->enableDebugger(); -error_reporting(~E_USER_DEPRECATED); // note ~ before E_USER_DEPRECATED -``` - - -Nette\SmartObject ------------------ - -Třídy frameworku teď místo dědění od `Nette\Object` používají novou traitu `Nette\SmartObject`, která se liší v několika věcech: - -- podporuje "emulované properties" jen pokud je na třídě zapsaná anotace `@property typ $nazev` -- nepodporuje "extension methods" -- nepodporuje "getReflection" -- nepodporuje získávání metod jako `$this->formSubmitted` (je třeba nahradit za klasický PHP callback `[$this, 'formSubmitted']` - -Použití těchto vlastností generuje E_USER_DEPRECATED a v příští verzi bude odstraněno. - - -Latte ------ -Latte generuje upozornění, pokud: - -- konstrukcí `{foreach $langs as $lang}` přepíšete parameter šablony `$lang` -- použijete již zastaralou vykřičníkovou konvenci `{!$name}` namísto `{$name|noescape}` -- použijete v šabloně `` nebo `{? ...}` namísto `{php ...}` -- použijete u bloku či include filtr, který je pro jiný content type ("vysvětlení":https://forum.nette.org/cs/26250-pojdte-otestovat-nette-2-4-rc#p174339) -- použijete `|nosafeurl` místo novějšího `|nocheck` -- použijete makro `{includeblock}`, které nahrazuje podobné makro `{import}` (importuje jen bloky a ne obsah okolo) -- dojde k chybě "Incompatible context" při používání bloků v jiném kontextu (například uvnitř HTML atributu bez uvozovek), než v jakém byl definován (například v běžném HTML textu) -- přistupujete k proměnné $template kvůli volání filtrů, což nahraďte za `($var|trim)` -- přistupujete k proměnné $template kvůli proměnné, což nahraďte za `$this->getParameter('xyz')` -- přistupujete k interní podtržítkové proměnné jako `$_control`, `$_form` atd. - - -Bootstrap a DI --------------- -- sekce (production, development) v jednom konfiguračním souboru, použijte dvojici konfiguračních souborů config.neon a config.local.neon -- dědičnost služeb -- Statement::setEntity() - - -Další ------ -- Negativní validační pravidla. Alternativou `~Form::FILLED` je `Form::BLANK`, nebo `~Form::EQUAL` můžete nahradit za `Form::NOT_EQUAL`. -- `Nette\Utils\Html::add()` se mění na `addHtml()` resp. `addText()`. -- V PhpGenerator metody `setDocuments(), getDocuments() a addDocument()` nahradily obdobné `setComment(), getComment() a addComment()`. -- Flag `Route::SECURED` a `Route::$defaultFlags` jsou deprecated. - - -Změny -===== - - -Application ------------ - -Route i SimpleRouter nyní generují v URL stejné HTTP/HTTPS schéma, s jakým se přistupuje ke stránce. Routy, které vyžadují určitý protokol, se dají napsat se schématem např. `Route('http://domain.cz/')`. - -U parametrů typu bool použitých v render/action metodách (tj. s výchozí hodnotou true nebo false) a persistentních parametrech se nyní rozlišuje mezi false a null, tj. pokud parametr v URL není uveden, jeho hodnota je nyní null, dříve byla false. Viz nette/application#107. - -Třída, kterou vrací `Presenter::getReflection()`, již není potomkem `Nette\Reflection\ClassType` a taktéž `getReflection()->getMethod()` není potomkem `Nette\Reflection\Method`. Metody `hasAnnotation` a `getAnnotation` neparsují text uvedený za názvem anotace, jako např. `abc` v `@param abc`, parsuje totiž jen text uvedený v závorkách `@param(abc)`. - - -Formuláře ---------- - -Pokud má prvek nastaveno nějaké pravidlo `addRule()` (tedy je efektivně povinný), musíte jej také označit jako povinný pomocí `setRequired()`. Dále `setRequired(false)` nyní udělá prvek volitelný, tj. pokud není vyplněn, neaplikují se validační pravidla. Lze tím nahradit větve `addCondition($form::FILLED)`. - -Už se nepoužívá interní proměnná `$_form`, kterou někdy bylo potřeba ručně předávat do inkludovaných šablon, což už není potřeba, nebo kvůli snippetům uvnitř formulářů, což nyní povede k chybě (např. `end() expects parameter 1 to be array, null given`). Pokud si kvůli snippetům předáváte do šablony `$template->_form = $form`, můžete to nahradit za `$template->getLatte()->addProvider('formsStack', [$form])`, nicméně jde stále jen o obezličku, korektní je použít `{snippetArea xyz}` a ten invalidovat společně se snippetem. - -Validátory `Form::EMAIL`, `URL`, `INTEGER` automaticky mění HTML atribut type na `email`, `url` resp. `number`. - -Interní parametr `do` se nyní u formulářů posílaných přes POST jmenuje `_do`, aby nedocházelo ke kolizi, a zároveň by nemělo docházet k mixováním formulářových prvků s persistentními parametry. - -Třída BaseControl už přímo definuje getControlPart() a getLabelPart() a standardně vracejí totéž, co getControl() a getLabel(). Makra `` už rovnou používají tyto -part metody. - -DefaultFormRenderer nyní správně vkládá do skupiny i vnořené komponenty (dříve se vnořené komponenty vykreslily až na konci formuláře), viz nette/forms#96. - -Pokud používáte vlastní TemplateFactory, aplikujte do ní "změny":[https://github.com/nette/application/compare/0cdccef046b1cc3465e9a78d8645ea5ed037822b...3aa6f65000c001ab0f8fdeb3695bc62f9931ae8a#files_bucket] vytvořené v originální továrně. - -Nezapomeňte zaktualizovat netteForms.js. - - -Latte ------ - -Filtry, které mají být aplikovány na blok textu (`{block}` nebo `{_}...{/_}`) či `{include}`, musejí jako první parametr přijímat objekt Latte\Runtime\FilterInfo, viz "příklad":https://github.com/nette/latte/blob/1423bd8974ce3132dee82aa0967f8afca49aca04/src/Latte/Runtime/Filters.php#L187-L199. - -Makro `{contentType}` je povolené pouze v hlavičce šablony a v elementu ` -``` - -Nette Framework přichází s revoluční technologií [Context-Aware Escaping |latte:safety-first#Kontextově sensitivní escapování], která vás provždy zbaví rizika Cross-Site Scriptingu. Všechny výstupy totiž ošetřuje automaticky a tak se nemůže stát, že by kodér na něco zapomněl. Příklad? Kodér vytvoří tuto šablonu: - -```latte -

          {$message}

          - - -``` - -Zápis `{$message}` znamená vypsání proměnné. V jiných frameworcích je nutné každé vypsání explicitně ošetřit a dokonce na každém místě jinak. V Nette Framework není potřeba ošetřovat nic, vše se udělá automaticky, správně a důsledně. Pokud dosadíme do proměnné `$message = 'Šířka 1/2"'`, framework vygeneruje HTML kód: - -```latte -

          Šířka 1/2"

          - - -``` - - -Cross-Site Request Forgery (CSRF) -================================= - -Útok Cross-Site Request Forgery spočívá v tom, že útočník naláká oběť na stránku, která nenápadně v prohlížeči oběti vykoná požadavek na server, na kterém je oběť přihlášena, a server se domnívá, že požadavek vykonala oběť o své vůli. A tak pod identitou oběti provede určitý úkon, aniž by ta o tom věděla. Může jít o změnu nebo smazání dat, odeslání zprávy atd. - -Nette Framework **automaticky chrání formuláře a signály v presenterech** před tímto typem útoku. A to tím, že zabraňuje jejich odeslání či vyvolání z jiné domény. Pokud chcete ochranu vypnout, použijte u formulářů: - -```php -$form->allowCrossOrigin(); -``` - -nebo v případě signálu přidejte anotaci `@crossOrigin`: - -```php -/** - * @crossOrigin - */ -public function handleXyz() -{ -} -``` - -V PHP 8 můžete použít také atributy: - -```php -use Nette\Application\Attributes\CrossOrigin; - -#[CrossOrigin] -public function handleXyz() -{ -} -``` - - -URL attack, control codes, invalid UTF-8 -======================================== - -Různé pojmy související se snahou útočníka podstrčit vaší webové aplikaci *škodlivý* vstup. Následky mohou být velmi různorodé, od poškození XML výstupů (např. nefunkční RSS kanály) přes získání citlivých informací z databáze nebo hesel. Obranou je důsledné ošetřování všech vstupů na úrovni jednotlivých bajtů. A ruku na srdce, kdo z vás to dělá? - -Nette Framework to dělá za vás a navíc automaticky. Nemusíte nastavovat vůbec nic a všechny vstupy budou ošetřené. - - -Session hijacking, session stealing, session fixation -===================================================== - -Se správou session je spojeno hned několik typů útoků. Útočník buď zcizí anebo podstrčí uživateli své session ID a díky tomu získá přístup do webové aplikace, aniž by znal heslo uživatele. Poté může v aplikaci provádět cokoliv, aniž by o tom uživatel věděl. Obrana spočívá ve správné konfiguraci serveru a PHP. - -Přičemž Nette Framework nakonfiguruje PHP automaticky. Programátor tak nemusí přemýšlet, kterak session správně zabezpečit a může se plně soustředit na tvorbu aplikace. Vyžaduje to však povolenou funkci `ini_set()`. - - -SameSite cookie -=============== - -SameSite cookies poskytují mechanismus jak rozpoznat, co vedlo k načtení stránky. Což je naprosto zásadní kvůli bezpečnosti. - -Příznak SameSite může mít tři hodnoty: `Lax`, `Strict` a `None` (ten vyžaduje HTTPS). Pokud požadavek na stránku přichází přímo z webu nebo uživatel otevře stránku přímým zadáním do adresního řádku nebo kliknutím na záložku, prohlížeč pošle serveru všechny cookies (tedy s příznaky `Lax`, `Strict` i `None`). Pokud se uživatel na web proklikne přes odkaz z jiného webu, předají se serveru cookies s příznaky `Lax` a `None`. Pokud požadavek vznikne jiným způsobem, jako je odeslání POST formuláře z jiného webu, načtení uvnitř iframe, pomocí JavaScriptu, atd., odešlou se jen cookies s příznakem `None`. - -Nette defaultně všechny cookie odesílá s příznakem `Lax`. - -{{leftbar: www:@menu-common}} diff --git a/nette/en/@home.texy b/nette/en/@home.texy index 5326d1bb7e..92b342d602 100644 --- a/nette/en/@home.texy +++ b/nette/en/@home.texy @@ -1,5 +1,5 @@ -Nette Documentation -******************* +''Nette 3.x Documentation'' +***************************
          @@ -8,19 +8,9 @@ Nette Documentation General ------- -- [Why Use Nette?|www:10-reasons-why-nette] -- [Create Your First Application! |quickstart:getting-started] - - -- [Packages & Installation |www:packages] -- [Maintenance and PHP |www:maintenance] -- [Release Notes |https://nette.org/releases] - [Upgrade Guide |migrations:] - [nette:Troubleshooting] -- [Who Creates Nette |https://nette.org/contributors] -- [History of Nette |www:history] -- [Get Involved |contributing:code] -- [API reference |https://api.nette.org/] +- [API reference |https://api.nette.org/nette/3.1/]
          @@ -30,17 +20,14 @@ General Nette Application ----------------- - [How do Applications Work? |application:how-it-works] -- [application:Bootstrap] +- [application:Bootstrapping] - [application:Presenters] - [application:Templates] -- [application:Modules] +- [Directory Structure |application:directory-structure] - [application:Routing] - [Creating URL Links |application:creating-links] - [Interactive Components |application:components] - [AJAX & Snippets |application:ajax] - - -- [Best Practices |best-practices:]
          @@ -54,13 +41,13 @@ Main Topics - [Latte: Templates |latte:] - [Tracy: Debugging Tool |tracy:] - [Forms|forms:] -- [Database |database:core] +- [Database |database:guide] - [Authenticating Users |security:authentication] - [Access Control |security:authorization] -- [http:Sessions] -- [HTTP request & response|http:] +- [Sessions |http:Sessions] +- [HTTP Request & Response|http:] - [Caching |caching:] -- [Emails Sending |mail:] +- [Sending Emails |mail:] - [Schema: Data Validation |schema:] - [PHP Code Generator |php-generator:] - [Tester: Unit Testing |tester:] @@ -80,11 +67,11 @@ Utilities - [utils:JSON] - [NEON|neon:] - [Password Hashing |security:passwords] -- [utils:SmartObject] - [PHP Reflection |utils:reflection] - [utils:Strings] - [Validators |utils:validators] - [RobotLoader |robot-loader:] +- [SmartObject |utils:smartobject] & [StaticClass |utils:StaticClass] - [SafeStream |safe-stream:] - [...others |utils:]
          @@ -94,4 +81,4 @@ Utilities {{toc:no}} {{description: Official Nette Documentation: describes how Nette works and the best practices for developing web applications.}} -{{maintitle: Nette Documentation}} +{{maintitle: Nette 3.x Documentation}} diff --git a/nette/en/@menu-topics.texy b/nette/en/@menu-topics.texy index 0b0ac06465..25477cf621 100644 --- a/nette/en/@menu-topics.texy +++ b/nette/en/@menu-topics.texy @@ -5,17 +5,17 @@ Main Topics - [Dependency Injection|dependency-injection:] - [Utilities |utils:] - [Forms|forms:] -- [Database |database:core] +- [Database |database:guide] - [Authenticating Users |security:authentication] - [Access Control |security:authorization] -- [http:Sessions] -- [HTTP request & response|http:] +- [Sessions |http:Sessions] +- [HTTP Request & Response|http:] - [Caching |caching:] -- [Emails Sending |mail:] +- [Sending Emails |mail:] - [Schema: Data Validation |schema:] - [PHP Code Generator |php-generator:] -- [Latte: templates |latte:] -- [Tracy: debugging |tracy:] -- [Tester: testing |tester:] +- [Latte: Templates |latte:] +- [Tracy: Debugging Tool |tracy:] +- [Tester: Unit Testing |tester:] diff --git a/nette/en/configuring.texy b/nette/en/configuring.texy index 7bf1cad491..b28ed62fb5 100644 --- a/nette/en/configuring.texy +++ b/nette/en/configuring.texy @@ -5,20 +5,20 @@ Configuring Nette An overview of all configuration options in the Nette Framework. The Nette components are configured using configuration files, which are usually written in [NEON|neon:format]. They are best edited in [editors that support it |best-practices:editors-and-tools#ide-editor]. -If you are using the full framework, the configuration will be [loaded during booting |application:bootstrap#di-container-configuration], if not, see [how to load the configuration |bootstrap:]. +If you are using the full framework, the configuration will be [loaded during booting |application:bootstrapping#di-container-configuration], if not, see [how to load the configuration |bootstrap:].
           "application .[prism-token prism-atrule]":[application:configuration#Application]: 	"Application .[prism-token prism-comment]"
          -"constants .[prism-token prism-atrule]":[application:configuration#Constants]: "Defines PHP constants .[prism-token prism-comment]"
          +"constants .[prism-token prism-atrule]":[application:configuration#Constants]: "Definition of PHP constants .[prism-token prism-comment]"
          "database .[prism-token prism-atrule]":[database:configuration]: "Database .[prism-token prism-comment]"
          "decorator .[prism-token prism-atrule]":[dependency-injection:configuration#Decorator]: "Decorator .[prism-token prism-comment]"
          "di .[prism-token prism-atrule]":[dependency-injection:configuration#DI]: "DI Container .[prism-token prism-comment]"
          -"extensions .[prism-token prism-atrule]":[dependency-injection:configuration#Extensions]: "Install additional DI extensions .[prism-token prism-comment]"
          +"extensions .[prism-token prism-atrule]":[dependency-injection:configuration#Extensions]: "Installation of additional DI extensions .[prism-token prism-comment]"
          "forms .[prism-token prism-atrule]":[forms:configuration]: "Forms .[prism-token prism-comment]"
          "http .[prism-token prism-atrule]":[http:configuration#HTTP Headers]: "HTTP Headers .[prism-token prism-comment]"
          -"includes .[prism-token prism-atrule]":[dependency-injection:configuration#Including files]: "Including files .[prism-token prism-comment]"
          -"latte .[prism-token prism-atrule]":[application:configuration#Latte]: "Latte .[prism-token prism-comment]"
          -"mail .[prism-token prism-atrule]":[mail:#Configuring]: "Mailing .[prism-token prism-comment]"
          +"includes .[prism-token prism-atrule]":[dependency-injection:configuration#Including Files]: "Including files .[prism-token prism-comment]"
          +"latte .[prism-token prism-atrule]":[application:configuration#Latte Templates]: "Latte Templates .[prism-token prism-comment]"
          +"mail .[prism-token prism-atrule]":[mail:#Configuration]: "Mailing .[prism-token prism-comment]"
          "parameters .[prism-token prism-atrule]":[dependency-injection:configuration#Parameters]: "Parameters .[prism-token prism-comment]"
          "php .[prism-token prism-atrule]":[application:configuration#PHP]: "PHP configuration options .[prism-token prism-comment]"
          "routing .[prism-token prism-atrule]":[application:configuration#Routing]: "Routing .[prism-token prism-comment]"
          @@ -29,7 +29,7 @@ If you are using the full framework, the configuration will be [loaded during bo "tracy .[prism-token prism-atrule]":[tracy:configuring#Nette Framework]: "Tracy Debugger .[prism-token prism-comment]"
          -To write a string containing the character `%`, you must escape it by doubling it to `%%`. .[note] +To write a string containing the character `%`, you must escape it by doubling it to `%%`. In version 3.0 it was necessary to double the `@` to distinguish it from the service name. .[note] {{leftbar: @menu-topics}} diff --git a/nette/en/glossary.texy b/nette/en/glossary.texy deleted file mode 100644 index 9ef8358c9a..0000000000 --- a/nette/en/glossary.texy +++ /dev/null @@ -1,157 +0,0 @@ -Glossary of Terms -***************** - - -AJAX ----- -Asynchronous JavaScript and XML - technology for client-server communication over the HTTP protocol without the need for reload of the whole page during each request. Despite the acronym, [#JSON] format is often used instead of XML. - - -Presenter Action ----------------- -Logical part of the [#presenter], performing one action, such as to show a product page, to sign out a user etc. One presenter can have more actions. - - -BOM ---- -So-called *byte order mask* is a special first character of a file and indicates byte order in the encoding. Some editors include it automatically, it's practically invisible, but it causes problems with headers and output sending from within PHP. You can use [Code Checker|code-checker:] for bulk removal. - - -Controller ----------- -Controller processes requests from user and on their basis it calls particular application logic (ie. [#model]), then it calls [#view] for data rendering. Analogy to controllers are [presenters|#presenter] in Nette Framework. - - -Cross-Site Scripting (XSS) --------------------------- -Cross-Site Scripting is a site disruption method using unescaped input. An attacker may inject his own HTML or JavaScript code and change the look of the page or even gather sensitive information about users. Protection against XSS is simple: consistent and correct escaping of all strings and inputs. - -Nette Framework comes up with a brand new technology of [Context-Aware Escaping |latte:safety-first#context-aware-escaping], which will get you rid of the Cross-Site Scripting risks forever. It escapes all inputs automatically based on a given context, so it's impossible for a coder to accidentally forget something. - - -Cross-Site Request Forgery (CSRF) ---------------------------------- -A Cross-Site Request Forgery attack is that the attacker lures the victim to visit a page that silently executes a request in the victim's browser to the server where the victim is currently logged in, and the server believes that the request was made by the victim at will. Server performs a certain action under the identity of the victim but without the victim realizing it. It can be changing or deleting data, sending a message, etc. - -Nette Framework **automatically protects forms and signals in presenters** from this type of attack. This is done by preventing them from being sent or called from another domain. - - -Dependency Injection --------------------- -Dependency Injection (DI) is a design pattern that tells you how to separate the creation of objects from their dependencies. That is, a class is not responsible for creating or initializing its dependencies, but instead those dependencies are provided by external code (which can include a [DI container |#Dependency Injection container]). The advantage is that it allows for greater code flexibility, better readability, and easier application testing because dependencies are easily replaceable and isolated from other parts of the code. For more information, see [What is Dependency Injection? |dependency-injection:introduction] - - -Dependency Injection Container ------------------------------- -A Dependency Injection container (also DI container or IoC container) is a tool that handles the creation and management of dependencies in an application (or [services |#service]). A container usually has a configuration that defines what classes are dependent on other classes, what specific dependency implementations to use, and how to create those dependencies. The container then creates these objects and provides them to the classes that need them. For more information, see [What is a DI container? |dependency-injection:container] - - -Escaping --------- -Escaping is conversion of characters with special meaning in given context to another equivalent sequences. Example: We want to write quotes into quotes-enclosed string. Because quotes have special meaning in context of the quotes-enclosed string, there is a need to use another equivalent sequence. Concrete sequence is determined by the context rules (e.g. `\"` in PHP's quotes-enclosed string, `"` in HTML attributes etc.). - - -Filter (Formerly Helper) ------------------------- -Filter function. In templates, [filter |latte:syntax#filters] is a function, that helps to alter or format data to the output form. Templates have several [standard filters |latte:filters] predefined. - - -Invalidation ------------- -Notice of a [#snippet] to rerender. In other context also clearing of a cache. - - -JSON ----- -Data exchange format based on JavaScript syntax (it's its subset). Exact specification can be found at www.json.org. - - -Component ---------- -Reusable part of an application. It can be a visual part of a page, as described in the [application:components] chapter, or the term can also stand for the class [Component |component-model:] (such a component doesn't have to be visual). - - -Control Characters ------------------- -Control characters are invisible characters, that can occur in a text and eventually to cause some problems. For their bulk removal from files, you can use [Code Checker|code-checker:], for their removal from a variable use function [Strings::normalize()|utils:strings#normalize]. - - -Events ------- -An event is an expected situation in the object, which when it occurs, the so-called handlers are called, i.e. callbacks reacting to the event ("sample":https://gist.github.com/dg/332cdd51bdf7d66a6d8003b134508a38). The event can be for example form submission, user login, etc. Events are thus a form of *Inversion of Control*. - -For example, a user login occurs in the `Nette\Security\User::login()` method. The `User` object has a public variable `$onLoggedIn`, which is an array to which anyone can add a callback. As soon as the user logs in, the `login()` method calls all callbacks in the array. The name of a variable in the form `onXyz` is a convention used throughout Nette. - - -Latte ------ -One of the most innovative [templating systems |latte:] ever. - - -Model ------ -Model represents data and function basis of the whole application. It includes the whole application logic (sometimes also referred to as a "business logic"). It's the **M** of **M**VC or MPV. Any user action (loging in, putting stuff to basket, change of a database value) represents an action of the model. - -Model manages its inner state and provides a public interface. By calling of this interface we can take or change its state. Model doesn't know about an existence of [#view] or [#controller], model is totally independent on them. - - -Model-View-Controller ---------------------- -Software architecture, that emerged in GUI applications development to separate the code for the flow control ([#controller]) from the code of the application logic ([#model]) and from the data rendering code ([#view]). That way the code is better understandable, it eases the future development and it allows to test separate parts separately. - - -Model-View-Presenter --------------------- -Architecture based on [#Model-View-Controller]. - - -Module ------- -[Module |application:modules] in Nette Framework represents a collection of presenters and templates, eventually also components and models, that serve data to a presenter. So it is certain logical part of an application. - -For example, an e-shop can have three modules: -1) Product catalogue with basket. -2) Administration for the customer. -3) Administration for the shopkeeper. - - -Namespace ---------- -Namespace is a feature of the PHP language from its version 5.3 and some other programming languages as well. It helps to avoid names collisions (e.g. two classes with the same name) when using different libraries together. See [PHP documentation |https://www.php.net/manual/en/language.namespaces.rationale.php] for further detail. - - -Presenter ---------- -Presenter is an object, that takes the [request |api:Nette\Application\Request] as translated by the router from the HTTP request and generates a [response |api:Nette\Application\Response]. Response can be an HTML page, picture, XML document, file, JSON, redirect or whatever you think of. - -By a presenter it is usually meant an descendant of the [api:Nette\Application\UI\Presenter] class. By requests it runs appropriate [actions |application:presenters#life-cycle-of-presenter] and renders templates. - - -Router ------- -Bi-directional translator between HTTP request / URL and presenter action. Bi-directional means, that it's not only possible to derive a [#presenter action] from the HTTP request, but also to generate appropriate URL for an action. See more in the chapter about [URL routing |application:routing]. - - -SameSite Cookie ---------------- -SameSite cookies provide a mechanism to recognize what led to the page load. It can have three values: `Lax`, `Strict` and `None` (the latter requires HTTPS). If the request to the page comes directly from the site or the user opens the page by typing directly into the address bar or clicking on a bookmark, the browser sends all cookies to the server (i.e. with the flags `Lax`, `Strict` and `None`). If the user clicks on the site via a link from another site, cookies with the `Lax` and `None` flags are passed to the server. If the request is made by other means, such as submitting a POST form from another site, loading inside an iframe, using JavaScript, etc., only cookies with the `None` flag are sent. - - -Service -------- -In the context of Dependency Injection, a service refers to an object that is created and managed by a DI container. A service can easily be replaced by another implementation, for example for testing purposes or to change the behavior of an application, without having to modify the code that uses the service. - - -Snippet -------- -Snippet of a page, that can be separately re-rendered during an [#AJAX] request. - - -View ----- -View is a layer of application, that is responsible for request results rendering. Usually it uses a templating system and it knows, how to render its components or results taken from the model. - - - -{{leftbar: www:@menu-common}} -{{priority: -2}} diff --git a/nette/en/troubleshooting.texy b/nette/en/troubleshooting.texy deleted file mode 100644 index 6437e09485..0000000000 --- a/nette/en/troubleshooting.texy +++ /dev/null @@ -1,153 +0,0 @@ -Troubleshooting -*************** - - -Nette Is Not Working, White Page Is Displayed ---------------------------------------------- -- Try putting `ini_set('display_errors', '1'); error_reporting(E_ALL);` after `declare(strict_types=1);` in the `index.php` file to force the display of errors -- If you still see a white screen, there is probably an error in the server setup and you will discover the reason in the server log. To be sure, check if PHP is working at all by trying to print something using `echo 'test';`. -- If you see an error *Server Error: We're sorry! ...*, continue with the next section: - - -Error 500 *Server Error: We're sorry! ...* ------------------------------------------- -This error page is displayed by Nette in production mode. If you are seeing it on a developer machine, [switch to developer mode |application:bootstrap#Development vs Production Mode]. - -If the error message contains `Tracy is unable to log error`, find out why the errors cannot be logged. You can do this by, for example, [switching |application:bootstrap#Development vs Production Mode] to developer mode and calling `Tracy\Debugger::log('hello');` after `$configurator->enableTracy(...)`. Tracy will tell you why it can't log. -The cause is usually [insufficient permissions |#Setting Directory Permissions] to write to the `log/` directory. - -If the sentence `Tracy is unable to log error` is not in the error message (anymore), you can find out the reason for the error in the log in the `log/` directory. - -One of the most common reasons is an outdated cache. While Nette cleverly automatically updates the cache in development mode, in production mode it focuses on maximizing performance, and clearing the cache after each code modification is up to you. Try to delete `temp/cache`. - - -Error `#[\ReturnTypeWillChange] attribute should be used` ---------------------------------------------------------- -This error occurs if you have upgraded PHP to version 8.1 but are using Nette, which is not compatible with it. So the solution is to update Nette to a newer version using `composer update`. Nette has supported PHP 8.1 since version 3.0. If you are using an older version (you can find out by looking in `composer.json`), [upgrade Nette |migrations:] or stay with PHP 8.0. - - -Setting Directory Permissions ------------------------------ -If you're developing on macOS or Linux (or any other Unix based system), you need to configure write privileges to the web server. Assuming your application is located in the default directory `/var/www/html` (Fedora, CentOS, RHEL) - -```shell -cd /var/www/html/MY_PROJECT -chmod -R a+rw temp log -``` - -On some Linux systems (Fedora, CentOS, ...) SELinux may be enabled by default. You may need to update SELinux policies, or set paths of `temp` and `log` directories with correct SELinux security context. Directories `temp` and `log` should be set to `httpd_sys_rw_content_t` context; for the rest of the application -- mainly `app` folder -- `httpd_sys_content_t` context will be enough. Run on the server as root: - -```shell -semanage fcontext -at httpd_sys_rw_content_t '/var/www/html/MY_PROJECT/log(/.*)?' -semanage fcontext -at httpd_sys_rw_content_t '/var/www/html/MY_PROJECT/temp(/.*)?' -restorecon -Rv /var/www/html/MY_PROJECT/ -``` - -Next, the SELinux boolean `httpd_can_network_connect_db` needs to be enabled to permit Nette to connect to the database over network. By default, it is disabled. The command `setsebool` can be used to perform this task, and if the option `-P` is specified, this setting will be persistent across reboots. - -```shell -setsebool -P httpd_can_network_connect_db on -``` - - -How to Change or Remove `www` Directory from URL? -------------------------------------------------- -The `www/` directory used in the sample projects in Nette is the so-called public directory or document-root of the project. It is the only directory whose contents are accessible to the browser. And it contains the `index.php` file, the entry point that starts a web application written in Nette. - -To run the application on the hosting, you need to set the document-root to this directory in the hosting configuration. Or, if the hosting has a pre-made folder for the public directory with a different name (for example `web`, `public_html` etc.), simply rename `www/`. - -The solution **isn't** to "get rid" of the `www/` folder using rules in the `.htaccess` file or in the router. If the hosting would not allow you to set document-root to a subdirectory (i.e. create directories one level above the public directory), look for another. You would otherwise be taking a significant security risk. It would be like living in an apartment where you can't close the front door and it's always wide open. - - -How to Configure a Server for Nice URLs? ----------------------------------------- -**Apache**: extension mod_rewrite must be allowed and configured in a `.htaccess` file. - -```apacheconf -RewriteEngine On -RewriteCond %{REQUEST_FILENAME} !-f -RewriteCond %{REQUEST_FILENAME} !-d -RewriteRule !\.(pdf|js|ico|gif|jpg|png|css|rar|zip|tar\.gz)$ index.php [L] -``` - -To alter Apache configuration with .htaccess files, the AllowOverride directive has to be enabled. This is the default behavior for Apache. - -**nginx**: the `try_files` directive should be used in server configuration: - -```nginx -location / { - try_files $uri $uri/ /index.php$is_args$args; # $is_args$args is important -} -``` - -Block `location` must be defined exactly once for each filesystem path in `server` block. If you already have a `location /` block in your configuration, add the `try_files` directive into the existing block. - - -Links Are Generated Without `https:` ------------------------------------- -Nette generates links with the same protocol as the current page is using. So on the `https://foo` page, it generates links starting with `https:` and vice versa. -If you're behind an HTTPS-stripping reverse proxy (for example, in Docker), then you need to [set up a proxy|http:configuration#HTTP proxy] in configuration to make the protocol detection work properly. - -If you use Nginx as a proxy, you need to have redirection set up like this: - -``` -location / { - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; - proxy_pass http://IP-aplikace:80; # IP or hostname of the server/container where the application is running -} -``` - -Next, you need to specify the IP proxy and, if applicable, the IP range of your local network where you run the infrastructure: - -```neon -http: - proxy: IP-proxy/IP-range -``` - - -Use of Characters { } in JavaScript ------------------------------------ -Characters `{` and `}` are used for writing Latte tags. Everything (except space and quotation mark) following the `{` character is considered a tag. If you need to print character `{` (often in JavaScript), you can put a space (or other empty character) right after `{`. By this you avoid interpreting it as a tag. - -If it's necessary to print these characters in a situation where they would be interpreted as a tag you can use specials tags to print out these characters - `{l}` for `{` and `{r}` for `}`. - -``` -{is tag} -{ is not tag } -{l}is not tag{r} -``` - - -Notice `Presenter::getContext() is deprecated` ----------------------------------------------- - -Nette is by far the first PHP framework that switched to dependency injection and led programmers to use it consistently, starting from the presenters. If a presenter needs a dependency, [it will ask for it|dependency-injection:passing-dependencies]. -In contrast, the way we pass the entire DI container to a class and it pulls the dependencies from it directly is considered an antipattern (it's called a service locator). -This way was used in Nette 0.x before the advent of dependency injection, and its relic is the method `Presenter::getContext()`, long ago marked as deprecated. - -If you port a very old Nette application, you may find that it still uses this method. So since version 3.1 of `nette/application` you will encounter the warning `Nette\Application\UI\Presenter::getContext() is deprecated, use dependency injection`, since version 4.0 you will encounter the error that the method does not exist. - -The clean solution, of course, is to redesign the application to pass dependencies using dependency injection. As a workaround, you can add your own `getContext()` method to your base presenter and bypass the message: - -```php -abstract BasePresenter extends Nette\Application\UI\Presenter -{ - private $context; - - public function injectContext(Nette\DI\Container $context) - { - $this->context = $context; - } - - public function getContext(): Nette\DI\Container - { - return $this->context; - } -} -``` - - -{{leftbar: www:@menu-common}} diff --git a/nette/en/vulnerability-protection.texy b/nette/en/vulnerability-protection.texy deleted file mode 100644 index 0eedf7cade..0000000000 --- a/nette/en/vulnerability-protection.texy +++ /dev/null @@ -1,100 +0,0 @@ -Vulnerability Protection -************************ - -.[perex] -Every now and then a major security flaw is announced or even abused. For sure that's a little bit unpleasant. If you care about the security of your web applications, Nette Framework is frankly the best choice for you. - - -Cross-Site Scripting (XSS) -========================== - -Cross-Site Scripting is a site disruption method using unescaped input. An attacker may inject his own HTML or JavaScript code and change the look of the page or even gather sensitive information about users. Protection against XSS is simple: consistent and correct escaping of all strings and inputs. Traditionally, it would be enough if your coder made just one slightest error and forgot once, and the whole website could be compromised. - -An example of such an injection may be slipping the user an altered URL, which inserts a "malicious" script. If an application does not escape its inputs properly, such a request would possibly execute a script on the client's side. This may, for example, lead to stolen identity. - -``` -https://example.com/?search= -``` - -Nette Framework comes up with a brand new technology of [Context-Aware Escaping |latte:safety-first#context-aware-escaping], which will get you rid of the Cross-Site Scripting risks forever. It escapes all inputs automatically based on a given context, so it's impossible for a coder to accidentally forget something. Consider the following template as an example: - -```latte -

          {$message}

          - - -``` - -The `{$message}` command prints a variable. Other frameworks do often force developers to explicitly declare escaping, and even what type of escaping based on the context. Yet in Nette Framework you don't need to declare anything. Everything is automatic, consistent and just right. If we set the variable to `$message = 'Width 1/2"'`, the framework will generate this HTML code: - -```latte -

          Width 1/2"

          - - -``` - - -Cross-Site Request Forgery (CSRF) -================================= - -A Cross-Site Request Forgery attack is that the attacker lures the victim to visit a page that silently executes a request in the victim's browser to the server where the victim is currently logged in, and the server believes that the request was made by the victim at will. Server performs a certain action under the identity of the victim but without the victim realizing it. It can be changing or deleting data, sending a message, etc. - -Nette Framework **automatically protects forms and signals in presenters** from this type of attack. This is done by preventing them from being sent or called from another domain. To turn off protection, use the following for forms: - -```php -$form->allowCrossOrigin(); -``` - -or in the case of a signal, add an annotation `@crossOrigin`: - -```php -/** - * @crossOrigin - */ -public function handleXyz() -{ -} -``` - -In PHP 8, you can also use attributes: - -```php -use Nette\Application\Attributes\CrossOrigin; - -#[CrossOrigin] -public function handleXyz() -{ -} -``` - - -URL Attack, Control Codes, Invalid UTF-8 -======================================== - -Different terms all related to the attacker's effort to give your application a "malicious" input. The results may vary greatly, from broken XML outputs (i.e. malfunctioned RSS stream) to getting sensitive information from an database to getting user passwords. Protection against these attacks is consistent UTF-8 check on byte level. And frankly, you would not do that without a framework, right? - -Nette Framework does this for you, automatically. You don't have to configure anything at all and your application will be safe. - - -Session Hijacking, Session Stealing, Session Fixation -===================================================== - -Session management involves a few types of attacks. The attacker may steal the victim's session ID or forge one and thus gain access to a web application without the actual password. Then the attacker may do whatever the user could, without any trace. The protection lies in proper configuration of both PHP and the web server itself. - -Nette Framework configures PHP automatically. Developers thus do not have to worry about how to make a session protected enough and can fully focus on the key parts of the application. This requires the `ini_set()` function to be enabled. - - -SameSite Cookie -=============== - -SameSite cookies provide a mechanism for recognizing what led to a page load. Which is absolutely crucial for security reasons. - -The SameSite flag can have three values: `Lax`, `Strict` and `None` (it requires HTTPS). If a request for a page comes directly from the web itself or the user opens the page by directly entering it in the address bar or clicking on a bookmark, the browser sends all cookies to the server (ie with the flags `Lax`, `Strict` and `None`). If a user comes to website via click on link from another website, cookies with the `Lax` and `None` flags will be passed to the server. If the request arises in another way such as sending a POST form from another origin, loading inside an iframe, using JavaScript, etc., only cookies with the `None` flag will be sent. - -By default, Nette sends all cookies with the `Lax` flag. - - -{{leftbar: www:@menu-common}} diff --git a/nette/meta.json b/nette/meta.json index 00060df255..c0ca0a2a85 100644 --- a/nette/meta.json +++ b/nette/meta.json @@ -1,3 +1,3 @@ { - "version": "4.0" + "version": "3.x" } diff --git a/php-generator/cs/@home.texy b/php-generator/cs/@home.texy index aa2a41ae43..e22670eacf 100644 --- a/php-generator/cs/@home.texy +++ b/php-generator/cs/@home.texy @@ -1,4 +1,4 @@ -Generátor PHP kódu +Nette PhpGenerator ******************
          @@ -91,7 +91,7 @@ private static $items = [1, 2, 3]; public ?array $list = null; ``` -A můžeme přidat [metody|#Signatury metod a funkcí]: +A můžeme přidat [metody |#Signatury metod a funkcí]: ```php $method = $class->addMethod('count') @@ -141,7 +141,7 @@ Vlastnosti určené pouze pro čtení zavedené v PHP 8.1 lze označit pomocí f ------ -Pokud přidaná vlastnost, konstanta, metoda nebo parametr již existují, vyhodí se výjimka. +Pokud přidaná vlastnost, konstanta, metoda nebo parametr již existují, budou přepsány. Členy třídy lze odebrat pomocí `removeProperty()`, `removeConstant()`, `removeMethod()` nebo `removeParameter()`. @@ -173,8 +173,8 @@ Interface nebo traita Lze vytvářet rozhraní a traity: ```php -$interface = new Nette\PhpGenerator\InterfaceType('MyInterface'); -$trait = new Nette\PhpGenerator\TraitType('MyTrait'); +$interface = Nette\PhpGenerator\ClassType::interface('MyInterface'); +$trait = Nette\PhpGenerator\ClassType::trait('MyTrait'); ``` Používání trait: @@ -207,7 +207,7 @@ Enums .{data-version:v3.6} Výčty, které přináší PHP 8.1, můžete snadno vytvořit takto: ```php -$enum = new Nette\PhpGenerator\EnumType('Suit'); +$enum = Nette\PhpGenerator\ClassType::enum('Suit'); $enum->addCase('Clubs'); $enum->addCase('Diamonds'); $enum->addCase('Hearts'); @@ -340,7 +340,7 @@ fn($a, $b) => $a + $b Signatury metod a funkcí ------------------------ -Metody reprezentuje třída [Method |api:Nette\PhpGenerator\Method]. Můžete nastavit viditelnost, návratovou hodnotu, přidat komentáře, [atributy|#Atributy] atd: +Metody reprezentuje třída [Method |api:Nette\PhpGenerator\Method]. Můžete nastavit viditelnost, návratovou hodnotu, přidat komentáře, [#atributy] atd: ```php $method = $class->addMethod('count') @@ -360,7 +360,7 @@ $method->addParameter('items', []) // $items = [] // function count(&$items = []) ``` -Pro definici tzv. variadics parametrů (nebo též splat operátor) slouží `setVariadics()`: +Pro definici tzv. variadics parametrů (nebo též splat operátor) slouží `setVariadic()`: ```php $method = $class->addMethod('count'); @@ -485,13 +485,10 @@ Potřebujete na míru upravit chování printeru? Vytvořte si vlastní podědě ```php class MyPrinter extends Nette\PhpGenerator\Printer { - public int $wrapLength = 120; - public string $indentation = "\t"; - public int $linesBetweenProperties = 0; - public int $linesBetweenMethods = 2; - public int $linesBetweenUseTypes = 0; - public bool $bracesOnNextLine = true; - public string $returnTypeColon = ': '; + protected $indentation = "\t"; + protected $linesBetweenProperties = 0; + protected $linesBetweenMethods = 1; + protected $returnTypeColon = ': '; } ``` @@ -544,7 +541,7 @@ class Demo } ``` -Můžete také předat parametry do `Literal` a nechat je zformátovat do platného kódu PHP pomocí [zástupných znaků|#Generování těl metod a funkcí]: +Můžete také předat parametry do `Literal` a nechat je zformátovat do platného kódu PHP pomocí [zástupných znaků |#Těla metod a funkcí]: ```php new Literal('substr(?, ?)', [$a, $b]); @@ -609,7 +606,7 @@ $class = new Nette\PhpGenerator\ClassType('Task'); $namespace->add($class); ``` -Pokud třída již existuje, vyhodí se výjimka. +Pokud třída již existuje, bude přepsána. Můžete definovat klauzule use: @@ -762,9 +759,9 @@ Těla funkcí a metod jsou ve výchozím stavu prázdná. Pokud je chcete také (vyžaduje instalaci balíčku `nikic/php-parser` a PhpGenerator verze 3.4): ```php -$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true); +$class = Nette\PhpGenerator\ClassType::withBodiesFrom(MyClass::class); -$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true); +$function = Nette\PhpGenerator\GlobalFunction::withBodyFrom('dump'); ``` @@ -810,12 +807,9 @@ echo $dumper->dump($var); // vypíše ['a', 'b', 123] Tabulka kompatibility --------------------- -PhpGenerator 4.0 je kompatibilní s PHP 8.0 až 8.2 -PhpGenerator 3.6 je kompatibilní s PHP 7.2 až 8.2 -PhpGenerator 3.2 – 3.5 je kompatibilní s PHP 7.1 až 8.0 -PhpGenerator 3.1 je kompatibilní s PHP 7.1 až 7.3 -PhpGenerator 3.0 je kompatibilní s PHP 7.0 až 7.3 -PhpGenerator 2.6 je kompatibilní s PHP 5.6 až 7.3 - - -{{leftbar: nette:@menu-topics}} +| verze | kompatibilní s PHP +|-----------|------------------- +| PhpGenerator 3.6 | PHP 7.2 až 8.2 +| PhpGenerator 3.2 – 3.5 | PHP 7.1 až 8.0 +| PhpGenerator 3.1 | PHP 7.1 až 7.3 +| PhpGenerator 3.0 | PHP 7.0 až 7.3 diff --git a/php-generator/cs/@meta.texy b/php-generator/cs/@meta.texy new file mode 100644 index 0000000000..08edde785b --- /dev/null +++ b/php-generator/cs/@meta.texy @@ -0,0 +1,2 @@ +{{sitename: Nette Dokumentace}} +{{leftbar: nette:@menu-topics}} diff --git a/php-generator/en/@home.texy b/php-generator/en/@home.texy index 78a18dfd47..2be0fb4d97 100644 --- a/php-generator/en/@home.texy +++ b/php-generator/en/@home.texy @@ -1,4 +1,4 @@ -PHP Code Generator +Nette PhpGenerator ******************
          @@ -141,7 +141,7 @@ Readonly properties introduced by PHP 8.1 can be marked via `setReadOnly()`. ------ -If the added property, constant, method or parameter already exist, it throws exception. +If the added property, constant, method or parameter already exist, it will be overwritten. Members can be removed using `removeProperty()`, `removeConstant()`, `removeMethod()` or `removeParameter()`. @@ -173,8 +173,8 @@ Interface or Trait You can create interfaces and traits: ```php -$interface = new Nette\PhpGenerator\InterfaceType('MyInterface'); -$trait = new Nette\PhpGenerator\TraitType('MyTrait'); +$interface = Nette\PhpGenerator\ClassType::interface('MyInterface'); +$trait = Nette\PhpGenerator\ClassType::trait('MyTrait'); ``` Using Traits: @@ -207,7 +207,7 @@ Enums .{data-version:v3.6} You can easily create the enums that PHP 8.1 brings: ```php -$enum = new Nette\PhpGenerator\EnumType('Suit'); +$enum = Nette\PhpGenerator\ClassType::enum('Suit'); $enum->addCase('Clubs'); $enum->addCase('Diamonds'); $enum->addCase('Hearts'); @@ -360,11 +360,11 @@ $method->addParameter('items', []) // $items = [] // function count(&$items = []) ``` -To define the so-called variadics parameters (or also the splat, spread, ellipsis, unpacking or three dots operator), use `setVariadics()`: +To define the so-called variadics parameters (or also the splat, spread, ellipsis, unpacking or three dots operator), use `setVariadic()`: ```php $method = $class->addMethod('count'); -$method->setVariadics(true); +$method->setVariadic(true); $method->addParameter('items'); ``` @@ -485,13 +485,10 @@ Need to customize printer behavior? Create your own by inheriting the `Printer` ```php class MyPrinter extends Nette\PhpGenerator\Printer { - public int $wrapLength = 120; - public string $indentation = "\t"; - public int $linesBetweenProperties = 0; - public int $linesBetweenMethods = 2; - public int $linesBetweenUseTypes = 0; - public bool $bracesOnNextLine = true; - public string $returnTypeColon = ': '; + protected $indentation = "\t"; + protected $linesBetweenProperties = 0; + protected $linesBetweenMethods = 1; + protected $returnTypeColon = ': '; } ``` @@ -609,7 +606,7 @@ $class = new Nette\PhpGenerator\ClassType('Task'); $namespace->add($class); ``` -If the class already exists, it throws exception. +If the class already exists, it will be overwritten. You can define use-statements: @@ -762,9 +759,9 @@ Function and method bodies are empty by default. If you want to load them as wel (it requires `nikic/php-parser` to be installed and PhpGenerator version 3.4): ```php -$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true); +$class = Nette\PhpGenerator\ClassType::withBodiesFrom(MyClass::class); -$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true); +$function = Nette\PhpGenerator\GlobalFunction::withBodyFrom('dump'); ``` @@ -810,11 +807,9 @@ echo $dumper->dump($var); // prints ['a', 'b', 123] Compatibility Table ------------------- -PhpGenerator 4.0 is compatible with PHP 8.0 to 8.2 -PhpGenerator 3.6 is compatible with PHP 7.2 to 8.2 -PhpGenerator 3.2 – 3.5 is compatible with PHP 7.1 to 8.0 -PhpGenerator 3.1 is compatible with PHP 7.1 to 7.3 -PhpGenerator 3.0 is compatible with PHP 7.0 to 7.3 -PhpGenerator 2.6 is compatible with PHP 5.6 to 7.3 - -{{leftbar: nette:@menu-topics}} +| version | compatible with PHP +|-----------|------------------- +| PhpGenerator 3.6 | PHP 7.2 až 8.2 +| PhpGenerator 3.2 – 3.5 | PHP 7.1 až 8.0 +| PhpGenerator 3.1 | PHP 7.1 až 7.3 +| PhpGenerator 3.0 | PHP 7.0 až 7.3 diff --git a/php-generator/en/@meta.texy b/php-generator/en/@meta.texy new file mode 100644 index 0000000000..91205786e5 --- /dev/null +++ b/php-generator/en/@meta.texy @@ -0,0 +1,2 @@ +{{sitename: Nette Documentation}} +{{leftbar: nette:@menu-topics}} diff --git a/php-generator/meta.json b/php-generator/meta.json index ce577b3ca8..11f028db00 100644 --- a/php-generator/meta.json +++ b/php-generator/meta.json @@ -1,5 +1,5 @@ { - "version": "4.0", + "version": "3.x", "repo": "nette/php-generator", "composer": "nette/php-generator" } diff --git a/pla/cs/vytvarime-kontaktny-formular.texy b/pla/cs/vytvarime-kontaktny-formular.texy deleted file mode 100644 index dc23d53abc..0000000000 --- a/pla/cs/vytvarime-kontaktny-formular.texy +++ /dev/null @@ -1,317 +0,0 @@ -Vytváříme kontaktní formulář -**************************** - -.[warning] -Tato stránka je již zastaralá a návod nemusí být funkční. - -.[perex] -Vytvoření kontaktního formuláře s odesláním na mail - formou **raw textu** i **html šablonou**. - -.[note] -Návod staví nad sandboxem **Nette 2.0.8** a PHP 5.3+. Stačí jej stáhnout a upravovat konkrétní soubory. - - -Jednoduše v presenteru -====================== - -Nejjednodušší a nejrychlejší přístup je vytvoření formuláře v presenteru - vytvoříme tedy komponentu `ContactForm`. Dále přidáme její zpracování třídou [Nette\Mail\Message|api:Nette\Mail\Message], která má na starosti vytváření a odesílání emailů. Více o odesílání emailů si můžete přečíst v [dokumentaci|mail:]. - - -**presenters/HomepagePresenter.php** - -```php -use Nette\Application\UI\Form; -use Nette\Mail\Message; - -class HomepagePresenter extends BasePresenter -{ - - /** - * Contact form - */ - protected function createComponentContactForm() - { - $form = new Form; - $form->addText('name', 'Jméno:') - ->addRule(Form::FILLED, 'Zadejte jméno'); - $form->addText('email', 'Email:') - ->addRule(Form::FILLED, 'Zadejte email') - ->addRule(Form::EMAIL, 'Email nemá správný formát'); - $form->addTextarea('message', 'Zpráva:') - ->addRule(Form::FILLED, 'Zadejte zprávu'); - $form->addSubmit('send', 'Odeslat'); - - $form->onSuccess[] = [$this, 'processContactForm']; - - return $form; - } - - - /** - * Process contact form, send message - * @param Form - */ - public function processContactForm(Form $form) - { - $values = $form->getValues(true); - - $message = new Message; - $message->addTo('test@gmail.com') - ->setFrom($values['email']) - ->setSubject('Zpráva z kontaktního formuláře') - ->setBody($values['message']) - ->send(); - - $this->flashMessage('Zpráva byla odeslána'); - $this->redirect('this'); - } - -} - -``` - - -Takto vytvořenou komponentu poté stačí přidat do šablony a můžeme vesele kontaktovat. - -**templates/Homepage/default.latte** - -```latte -{block content} - -{control contactForm} -``` - - -HTML šablona emailu -------------------- - -Pokud chceme v emailu využít HTML, musíme použít vlastní šablonu. V ní definujeme proměnné (ty jí musíme předat) a předmět v tagu ``. Šablonu umístíme např. do: - -**templates/email/emailTemplate.latte** - -```latte -<html> - <head> - <title>{$title} - - - -
            -
          • - Jméno: {$values['name']} -
          • -
          • - Email: {$values['email']} -
          • -
          • - Zpráva: {$values['message']} -
          • -
          - - - -``` - -Ještě upravíme metodu pro zpracování formuláře. - -**presenters/HomepagePresenter.php** - - -```php - -/** - * Process contact form, send message with custom template - * @param Form - */ -public function processContactForm(Form $form) -{ - $values = $form->getValues(true); - - $message = new Message; - $message->addTo('test@gmail.com') - ->setFrom($values['email']); - - $template = $this->createTemplate(); - $template->setFile(__DIR__ . '/../templates/emails/emailTemplate.latte'); - $template->title = 'Zpráva z kontaktního formuláře'; - $template->values = $values; - - $message->setHtmlBody($template) - ->send(); - - $this->flashMessage('Zpráva byla odeslána'); - $this->redirect('this'); -} -``` - - - - - - - - -/--comment - - -Jako komponenta -=============== - -Email s šablonou v komponentě - - -Šikovná komponenta ------------------- - -Formuláře je vhodné pro přehlednost a lepší rozšiřitelnost umisťovat do samostatné složky, např. `app/Forms`. Jelikož v tomto návodu bude potřeba vytváření šablon, využijeme k tomu samostatnou komponentu, kterou umístíme do `app/Components`. - -Zde vytvoříme složku `ContactFormControl` a v ní soubory: - -**ContactFormControl.php** - -/-php -use Nette\Application\UI\Control; -use Nette\Application\UI\Form; -use Nette\Mail\Message; - -class ContactFormComponent extends Control -{ - - protected function createComponentContactForm() - { - $form = new Form; - $form->addText('name', 'Jméno:') - ->addRule(Form::FILLED, 'Zadejte jméno'); - $form->addText('email', 'Email:') - ->addRule(Form::FILLED, 'Zadejte email') - ->addRule(Form::EMAIL, 'Email nemá správný formát'); - $form->addTextarea('message', 'Zpráva:') - ->addRule(Form::FILLED, 'Zadejte zprávu'); - $form->addSubmit('send', 'Odeslat'); - - $form->onSuccess[] = [$this, 'processContactForm']; - - return $form; - } - - - public function processContactForm(Form $form) - { - $settings = $this->presenter->context->parameters['email']; - $values = $form->values; - - $message = new Message; - $message->addTo($settings['to']) - ->setFrom($values['email']); - - // 1. způsob - raw text - $message->setSubject($settings['subject']) - ->setBody($values['message']); - - // 2. způsob - html - $template = $this->createTemplate(); - $template->setFile(__DIR__ . '/emailTemplate.latte'); - $template->title = $settings['subject']; - $template->values = $values; - $message->setHtmlBody($template); - - $message->send(); - - $this->presenter->flashMessage('Zpráva byla odeslána'); - $this->redirect('this'); - } - - - public function render() - { - $this->template->setFile(__DIR__ . '/ContactFormControl.latte'); - $this->template->render(); - } - -} - - - -**ContactFormControl.latte** -/-html -{control contactForm} - - -**emailTemplate.latte** -/-html - - - {$title} - - - -
            -
          • - Jméno: {$values['name']} -
          • -
          • - Email: {$values['email']} -
          • -
          • - Zpráva: {$values['message']} -
          • -
          - - - -\- - - -Presenter ---------- - -Poté formulář zavoláme v presenteru a vytvoříme komponentu: - -**HomepagePresenter.php** - -```php -class HomepagePresenter extends BasePresenter -{ - - protected function createComponentContactFormControl() - { - return new ContactFormComponent; - } - - -} - -``` - - -Šablona -------- - -Komponentu přidáme do šalbony: - -**Homepage/default.latte** -```latte -{block content} - {control contactFormControl} -{/block} -``` - - -Config ------- - -**app/config/config.neon** -```neon -parameters: - email: - to: my@email.com - subject: Kontaktní formulář -``` - - -SMTP odesílání? ---------------- - -Link někam, toto už tu je... - -\-- diff --git a/pla/meta.json b/pla/meta.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/pla/meta.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/quickstart/cs/@home.texy b/quickstart/cs/@home.texy deleted file mode 100644 index a0e6db89b0..0000000000 --- a/quickstart/cs/@home.texy +++ /dev/null @@ -1 +0,0 @@ -{{redirect:getting-started}} diff --git a/quickstart/cs/@left-menu.texy b/quickstart/cs/@left-menu.texy deleted file mode 100644 index 97d1ea6abc..0000000000 --- a/quickstart/cs/@left-menu.texy +++ /dev/null @@ -1,7 +0,0 @@ -- [Píšeme první aplikaci! |getting-started] -- [Úvodní stránka blogu |home-page] -- [Stránka s příspěvkem |single-post] -- [Komentáře |comments] -- [Vytváření a editování příspěvků |creating-posts] -- [Model] -- [Autentifikace |authentication] diff --git a/quickstart/cs/authentication.texy b/quickstart/cs/authentication.texy deleted file mode 100644 index 8974a3cbe0..0000000000 --- a/quickstart/cs/authentication.texy +++ /dev/null @@ -1,180 +0,0 @@ -Autentifikace -************* - -Nette poskytuje způsob jak naprogramovat autentifikaci na našich stránkách, ale do ničeho nás nenutí. Implementace je pouze na nás. Nette obsahuje rozhraní `Nette\Security\Authenticator`, které vyžaduje pouze jednu metodu `authenticate`, která ověřuje uživatele jakkoliv budeme chtít. - -Existuje mnoho možností, jak může být uživatel ověřen. Nejčastější způsob ověření je pomocí hesla (uživatel poskytne své jméno, nebo e-mail a heslo), ale jsou zde i jiné způsoby. Možná znáte tlačítka typu "Přihlásit pomocí Facebooku", nebo přihlášení pomocí Google/Twitter/GitHub na některých stránkách. S Nette můžeme mít jakoukoliv přihlašovací metodu, nebo je klidně můžeme kombinovat. Je to jen na nás. - -Obyčejně bychom si napsali vlastní authenticator, ale pro tento jednoduchý malý blog použijeme vestavěný authenticator, který přihlašuje na základě hesla a uživatelského jména uloženého v konfiguračním souboru. Hodí se pro testovací účely. Přidáme tedy následující *security* sekci do konfiguračního souboru `config/common.neon`: - - -```neon .{file:config/common.neon} -security: - users: - admin: secret # user 'admin', password 'secret' -``` - -Nette automaticky vytvoří službu v DI kontejneru. - - -Přihlašovací formulář -===================== - -Nyní máme autentifikaci připravenu a musíme připravit uživatelské rozhraní pro přihlášení. Vytvořme tedy nový presenter s názvem *SignPresenter*, který: - -- zobrazí přihlašovací formulář (s přihlašovacím jménem a heslem) -- po odeslání formuláře ověří uživatele -- poskytne možnost odhlášení - -Začneme s přihlašovacím formulářem. Již víme, jak formuláře v presenterech fungují. Vytvoříme si tedy presenter `SignPresenter` a zapíšeme metodu `createComponentSignInForm`. Měl by vypadat nějak takto: - -```php .{file:app/Presenters/SignPresenter.php} -addText('username', 'Uživatelské jméno:') - ->setRequired('Prosím vyplňte své uživatelské jméno.'); - - $form->addPassword('password', 'Heslo:') - ->setRequired('Prosím vyplňte své heslo.'); - - $form->addSubmit('send', 'Přihlásit'); - - $form->onSuccess[] = [$this, 'signInFormSucceeded']; - return $form; - } -} -``` - -Jsou zde políčka pro uživatelské jméno a heslo. - - -Šablona -------- - -Formulář se bude vykreslovat v šabloně `in.latte`: - -```latte .{file:app/Presenters/templates/Sign/in.latte} -{block content} -

          Přihlášení

          - -{control signInForm} -``` - - -Přihlašovací callback ---------------------- - -Dále doplníme callback pro přihlášení uživatele, který bude volán hned po úspěšném odeslání formuláře. - -Callback pouze převezme uživatelské jméno a heslo, které uživatel vyplnil a předá je authenticatoru. Po přihlášení přesměrujeme na úvodní stránku. - -```php .{file:app/Presenters/SignPresenter.php} -public function signInFormSucceeded(Form $form, \stdClass $data): void -{ - try { - $this->getUser()->login($data->username, $data->password); - $this->redirect('Homepage:'); - - } catch (Nette\Security\AuthenticationException $e) { - $form->addError('Nesprávné přihlašovací jméno nebo heslo.'); - } -} -``` - -Metoda [User::login() |api:Nette\Security\User::login()] vyhodí výjimku, pokud uživatelské jméno a heslo nesouhlasí s údaji v konfiguračním souboru. Jak již víme, toto může vyústit v červenou chybovou stránku, nebo v produkčním módu ve zprávu informující o server erroru. To však nechceme. Proto tuto výjimku zachytíme a předáme hezkou, uživatelsky přívětivou chybovou zprávu do formuláře. - -Jakmile dojde k chybě ve formuláři, stránka s formulářem se překreslí a nad formulářem se zobrazí hezká zpráva informující uživatele, že vyplnil špatné přihlašovací jméno nebo heslo. - - -Zabezpečení presenterů -====================== - -Zabezpečíme formulář pro přidávání a editování příspěvků. Ten je definován v presenteru `EditPresenter`. Cílem je znemožnit přístup na stránku uživatelům, kteří nejsou přihlášeni. - -Vytvoříme metodu `startup()`, která se spouští ihned na začátku [životního cyklu presenteru|application:presenters#zivotni-cyklus-presenteru]. Ta přesměruje nepřihlášené uživatele na formulář s přihlášením. - -```php .{file:app/Presenters/EditPresenter.php} -public function startup(): void -{ - parent::startup(); - - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in'); - } -} -``` - - -Skrytí odkazů -------------- - -Neautorizovaný uživatel už nemůže vidět stránku *create* ani *edit*, ale stále na ně může vidět odkazy. Ty bychom měli také schovat. Jeden takový odkaz je v šabloně `app/Presenters/templates/Homepage/default.latte` a měli by jej vidět pouze přihlášení uživatelé. - -Můžeme jej schovat využitím *n:atributu* jménem `n:if`. Pokud je tato podmínka `false`, celý tag ``, včetně obsahu, zůstane skrytý. - -```latte -Vytvořit příspěvek -``` - -což je zkratka následujícího zápisu (neplést s `tag-if`): - -```latte -{if $user->isLoggedIn()}Vytvořit příspěvek{/if} -``` - -Stejným způsobem skryjeme také odkaz v šabloně `app/Presenters/templates/Post/show.latte`. - - -Odkaz na přihlášení -=================== - -Jak se vlastně dostaneme na přihlašovací stránku? Není zde žádný odkaz, který by na ni vedl. Tak si ho tedy přidáme do šablony `@layout.latte`. Pokuste se najít vhodné místo - může to být téměř kdekoliv. - -```latte .{file:app/Presenters/templates/@layout.latte} -... - -... -``` - -Pokud není uživatel přihlášen, zobrazí se odkaz "Přihlásit". V opačném případě se zobrazí odkaz "Odhlásit". Tuto akci také doplníme do `SignPresenter`. - -Jelikož uživatele po odhlášení okamžitě přesměrujeme, není potřeba žádná šablona. Odhlášení vypadá takto: - -```php .{file:app/Presenters/SignPresenter.php} -public function actionOut(): void -{ - $this->getUser()->logout(); - $this->flashMessage('Odhlášení bylo úspěšné.'); - $this->redirect('Homepage:'); -} -``` - -Pouze se zavolá metoda `logout()` a následně se zobrazí hezká zpráva potvrzující úspěšné odhlášení. - - -Shrnutí -======= - -Máme odkaz pro přihlášení a také odhlášení uživatele. K ověření jsme použili vestavěný authentikátor a přihlašovací údaje máme v konfiguračním souboru, jelikož se jedná o jednoduchou testovací aplikaci. Také jsme zabezpečili editační formuláře, takže přidávat a editovat příspěvky mohou pouze přihlášení uživatelé. - -.[note] -Zde si můžete přečíst více o [přihlašování uživatelů |security:authentication] a [Ověřování oprávnění |security:authorization]. - -{{priority: -1}} -{{sitename: Nette Quickstart}} diff --git a/quickstart/cs/comments.texy b/quickstart/cs/comments.texy deleted file mode 100644 index da94081a2e..0000000000 --- a/quickstart/cs/comments.texy +++ /dev/null @@ -1,172 +0,0 @@ -Komentáře -********* - -Nahráli jsme blog na webserver a publikovali několik velmi zajímavých příspěvků pomocí Admineru. Lidé čtou náš blog a jsou z něho velmi nadšení. Dostáváme každý den mnoho e-mailů s pochvalami. Ale k čemu je všechna tato chvála, pokud ji máme pouze v e-mailu a nikdo si ji nemůže přečíst? Bylo by lepší, kdyby mohl čtenář článek přímo komentovat, takže by si mohl každý přečíst, jak jsme úžasní. - -Pojďme tedy naprogramovat komentáře. - - -Tvorba nové tabulky -=================== - -Nažhavíme Adminer a vytvoříme tabulku `comments` s těmito sloupci: - -- `id` int, zaškrtneme autoincrement (AI) -- `post_id`, cizí klíč, který odkazuje na tabulku `posts` -- `name` varchar, length 255 -- `email` varchar, length 255 -- `content` text -- `created_at` timestamp - -Tabulka by tedy měla vypadat nějak takto: - -[* adminer-comments.webp *] - -Nezapomeňte opět použít úložiště InnoDB. - -```sql -CREATE TABLE `comments` ( - `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY, - `post_id` int(11) NOT NULL, - `name` varchar(250) NOT NULL, - `email` varchar(250) NOT NULL, - `content` text NOT NULL, - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) -) ENGINE=InnoDB CHARSET=utf8; -``` - - -Formulář pro komentování -======================== - -Prvně musíme vytvořit formulář, který umožní uživatelům příspěvky komentovat. Nette Framework má úžasnou podporu pro formuláře. Můžeme je nakonfigurovat v presenteru a vykreslit v šabloně. - -Nette Framework využívá koncept *komponent*. **Komponenta** je znovupoužitelná třída, nebo část kódu, který může být přiložen k jiné komponentě. Dokonce i presenter je komponenta. Každá komponenta je vytvořena prostřednictvím továrny. Vytvoříme si tedy továrnu pro formulář na komentáře v presenteru `PostPresenter`. - -```php .{file:app/Presenters/PostPresenter.php} -protected function createComponentCommentForm(): Form -{ - $form = new Form; // means Nette\Application\UI\Form - - $form->addText('name', 'Jméno:') - ->setRequired(); - - $form->addEmail('email', 'E-mail:'); - - $form->addTextArea('content', 'Komentář:') - ->setRequired(); - - $form->addSubmit('send', 'Publikovat komentář'); - - return $form; -} -``` - -Pojďme si to zase trochu vysvětlit. První řádka vytvoří novou instanci komponenty `Form`. Následující metody připojují HTML inputy do definice tohoto formuláře. `->addText` se vykreslí jako `` s ``. Jak již zřejmě správně odhadujete, tak `->addTextArea` se vykreslí jako `